コミットを比較
6 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| f3d3f2d301 | |||
| 791b52e894 | |||
| 6750692445 | |||
| 01b063f473 | |||
| c06d73fc6c | |||
| 3b5ad3b805 |
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 559 KiB |
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 146 KiB |
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 1.2 MiB |
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 188 KiB |
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 201 KiB |
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 196 KiB |
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 179 KiB |
@@ -30,7 +30,7 @@ export type GekanatorQuestionSource =
|
|||||||
| 'ai_generated'
|
| 'ai_generated'
|
||||||
| 'admin_curated'
|
| 'admin_curated'
|
||||||
|
|
||||||
export type GekanatorPerformanceMode = 'normal'
|
export type GekanatorPerformanceMode = 'lite' | 'normal'
|
||||||
|
|
||||||
export type GekanatorQuestionCondition =
|
export type GekanatorQuestionCondition =
|
||||||
| { type: 'tag'; key: string }
|
| { type: 'tag'; key: string }
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import type { FC } from 'react'
|
|||||||
import type { GekanatorAnswerLog,
|
import type { GekanatorAnswerLog,
|
||||||
GekanatorAnswerValue,
|
GekanatorAnswerValue,
|
||||||
GekanatorExtraQuestion,
|
GekanatorExtraQuestion,
|
||||||
|
GekanatorPerformanceMode,
|
||||||
GekanatorQuestionCondition,
|
GekanatorQuestionCondition,
|
||||||
GekanatorQuestionKind,
|
GekanatorQuestionKind,
|
||||||
GekanatorQuestion,
|
GekanatorQuestion,
|
||||||
@@ -161,6 +162,7 @@ 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> = {
|
||||||
@@ -389,8 +391,14 @@ const storeRecentGameSummary = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const loadBackgroundMotionMode = (): BackgroundMotionMode => {
|
const loadBackgroundMotionMode = (
|
||||||
const fallbackMode = 'on'
|
performanceMode?: GekanatorPerformanceMode,
|
||||||
|
): 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)
|
||||||
@@ -406,6 +414,38 @@ const loadBackgroundMotionMode = (): BackgroundMotionMode => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -1015,9 +1055,11 @@ 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
|
||||||
acceptedQuestions: GekanatorQuestion[] },
|
performanceMode: GekanatorPerformanceMode
|
||||||
|
acceptedQuestions: GekanatorQuestion[] },
|
||||||
): GekanatorQuestion[] => {
|
): GekanatorQuestion[] => {
|
||||||
const total = candidateIds.length
|
const total = candidateIds.length
|
||||||
if (total === 0)
|
if (total === 0)
|
||||||
@@ -1047,16 +1089,23 @@ 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)
|
||||||
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
|
if (performanceMode === 'normal')
|
||||||
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
|
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
|
||||||
|
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
|
||||||
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
|
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
|
||||||
titleLengths.push (titleLength)
|
titleLengths.push (titleLength)
|
||||||
if (materialIndex.titleAsciiPostIds.has (postId))
|
if (materialIndex.titleAsciiPostIds.has (postId))
|
||||||
++asciiCount
|
++asciiCount
|
||||||
})
|
})
|
||||||
|
|
||||||
const tagCap = total >= 120 ? 128 : 96
|
const tagCap =
|
||||||
const titleTermCap = total >= 80 ? 10 : total >= 24 ? 14 : 20
|
performanceMode === 'lite'
|
||||||
|
? 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
|
||||||
@@ -1132,16 +1181,17 @@ const buildQuestionsForCandidateIds = (
|
|||||||
materialIndex }))
|
materialIndex }))
|
||||||
})
|
})
|
||||||
|
|
||||||
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
|
if (performanceMode === 'normal')
|
||||||
.filter (([term]) => String (term).length <= 24)
|
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
|
||||||
.forEach (([term]) => {
|
.filter (([term]) => String (term).length <= 24)
|
||||||
questions.push (buildIndexedQuestion ({
|
.forEach (([term]) => {
|
||||||
condition: { type: 'title-contains', text: String (term) },
|
questions.push (buildIndexedQuestion ({
|
||||||
text: `題名に「${ term }」が含まれる?`,
|
condition: { type: 'title-contains', text: String (term) },
|
||||||
kind: 'title',
|
text: `題名に「${ term }」が含まれる?`,
|
||||||
priorityWeight: .96,
|
kind: 'title',
|
||||||
materialIndex }))
|
priorityWeight: .96,
|
||||||
})
|
materialIndex }))
|
||||||
|
})
|
||||||
|
|
||||||
return mergeQuestions ([...questions, ...acceptedQuestions])
|
return mergeQuestions ([...questions, ...acceptedQuestions])
|
||||||
}
|
}
|
||||||
@@ -1565,33 +1615,41 @@ const contradictionPenaltyFor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const chooseQuestion = (
|
const chooseQuestion = ({
|
||||||
{ posts,
|
posts,
|
||||||
questions,
|
questions,
|
||||||
scores,
|
scores,
|
||||||
answers,
|
answers,
|
||||||
askedIds,
|
askedIds,
|
||||||
gameSeed,
|
gameSeed,
|
||||||
recentFirstQuestionPenaltyById,
|
recentFirstQuestionPenaltyById,
|
||||||
userPriorWeights,
|
userPriorWeights,
|
||||||
materialIndex,
|
performanceMode,
|
||||||
matchIndex }: { posts: Post[]
|
materialIndex,
|
||||||
questions: GekanatorQuestion[]
|
matchIndex,
|
||||||
scores: Map<number, number>
|
}: {
|
||||||
answers: GekanatorAnswerLog[]
|
posts: Post[]
|
||||||
askedIds: Set<string>
|
questions: GekanatorQuestion[]
|
||||||
gameSeed: string
|
scores: Map<number, number>
|
||||||
recentFirstQuestionPenaltyById: Map<string, number>
|
answers: GekanatorAnswerLog[]
|
||||||
userPriorWeights: Map<number, number>
|
askedIds: Set<string>
|
||||||
materialIndex: GekanatorQuestionMaterialIndex
|
gameSeed: string
|
||||||
matchIndex: GekanatorMatchIndex },
|
recentFirstQuestionPenaltyById: Map<string, number>
|
||||||
): GekanatorQuestion | null => {
|
userPriorWeights: Map<number, number>
|
||||||
|
performanceMode: GekanatorPerformanceMode
|
||||||
|
materialIndex: GekanatorQuestionMaterialIndex
|
||||||
|
matchIndex: GekanatorMatchIndex
|
||||||
|
}): 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 = (candidates: Post[]): Set<string> => {
|
const redundantSignatures = (
|
||||||
|
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))
|
||||||
@@ -1610,6 +1668,89 @@ 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)
|
||||||
@@ -2220,6 +2361,7 @@ const nextQuestionPlanFor = (
|
|||||||
gameSeed,
|
gameSeed,
|
||||||
recentFirstQuestionPenaltyById,
|
recentFirstQuestionPenaltyById,
|
||||||
userPriorWeights,
|
userPriorWeights,
|
||||||
|
performanceMode,
|
||||||
materialIndex,
|
materialIndex,
|
||||||
matchIndex,
|
matchIndex,
|
||||||
lastGuessQuestionCount,
|
lastGuessQuestionCount,
|
||||||
@@ -2234,6 +2376,7 @@ 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
|
||||||
@@ -2296,6 +2439,7 @@ 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)
|
||||||
@@ -2361,6 +2505,7 @@ const nextQuestionPlanFor = (
|
|||||||
gameSeed,
|
gameSeed,
|
||||||
recentFirstQuestionPenaltyById,
|
recentFirstQuestionPenaltyById,
|
||||||
userPriorWeights,
|
userPriorWeights,
|
||||||
|
performanceMode,
|
||||||
materialIndex,
|
materialIndex,
|
||||||
matchIndex })
|
matchIndex })
|
||||||
|
|
||||||
@@ -2963,8 +3108,10 @@ 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 ())
|
() => loadBackgroundMotionMode (loadPerformanceMode ()))
|
||||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState (false)
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState (false)
|
||||||
const [gameSeed, setGameSeed] = useState (
|
const [gameSeed, setGameSeed] = useState (
|
||||||
storedGame?.gameSeed ?? createGameSeed ())
|
storedGame?.gameSeed ?? createGameSeed ())
|
||||||
@@ -3181,6 +3328,17 @@ 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])
|
||||||
@@ -3203,6 +3361,9 @@ 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) => {
|
||||||
@@ -3215,10 +3376,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return penalties
|
return penalties
|
||||||
}, [recentGames])
|
}, [performanceMode, recentGames])
|
||||||
const userPriorWeights = useMemo (
|
const userPriorWeights = useMemo (
|
||||||
() => userPriorWeightsFor (posts, recentGames),
|
() => performanceMode === 'lite'
|
||||||
[posts, recentGames])
|
? new Map<number, number> ()
|
||||||
|
: 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])
|
||||||
@@ -3234,6 +3397,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
gameSeed,
|
gameSeed,
|
||||||
recentFirstQuestionPenaltyById,
|
recentFirstQuestionPenaltyById,
|
||||||
userPriorWeights,
|
userPriorWeights,
|
||||||
|
performanceMode,
|
||||||
materialIndex,
|
materialIndex,
|
||||||
matchIndex: acceptedQuestionMatchIndex,
|
matchIndex: acceptedQuestionMatchIndex,
|
||||||
lastGuessQuestionCount,
|
lastGuessQuestionCount,
|
||||||
@@ -3241,7 +3405,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, materialIndex, acceptedQuestionMatchIndex,
|
userPriorWeights, performanceMode, materialIndex, acceptedQuestionMatchIndex,
|
||||||
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
||||||
const winningRunTargetPost = useMemo (
|
const winningRunTargetPost = useMemo (
|
||||||
() => questionPlan.winningRunTargetId === null
|
() => questionPlan.winningRunTargetId === null
|
||||||
@@ -3294,28 +3458,35 @@ 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 && reviewCorrectPostId !== null)
|
reviewGuessedPostId !== null
|
||||||
? reviewGuessedPostId === reviewCorrectPostId
|
&& reviewCorrectPostId !== null
|
||||||
: null)
|
? reviewGuessedPostId === reviewCorrectPostId
|
||||||
|
: null)
|
||||||
const effectiveBackgroundMotionMode =
|
const effectiveBackgroundMotionMode =
|
||||||
backgroundMotionMode === 'off'
|
performanceMode === 'lite'
|
||||||
? 'off'
|
? 'off'
|
||||||
: (prefersReducedMotion
|
: backgroundMotionMode === 'off'
|
||||||
? 'calm'
|
? 'off'
|
||||||
: backgroundMotionMode)
|
: prefersReducedMotion
|
||||||
|
? 'calm'
|
||||||
|
: backgroundMotionMode
|
||||||
const backgroundPosts = useMemo (
|
const backgroundPosts = useMemo (
|
||||||
() => backgroundPostsFor ({
|
() => performanceMode === 'lite'
|
||||||
|
? []
|
||||||
|
: backgroundPostsFor ({
|
||||||
phase,
|
phase,
|
||||||
eligiblePosts,
|
eligiblePosts,
|
||||||
availablePosts,
|
availablePosts,
|
||||||
displayedGuess,
|
displayedGuess,
|
||||||
reviewCorrectPost,
|
reviewCorrectPost,
|
||||||
reviewGuessedPost }),
|
reviewGuessedPost }),
|
||||||
[phase, eligiblePosts, availablePosts, displayedGuess, reviewCorrectPost,
|
[performanceMode, phase, eligiblePosts, availablePosts, displayedGuess,
|
||||||
reviewGuessedPost])
|
reviewCorrectPost, reviewGuessedPost])
|
||||||
const backgroundVisualSeed =
|
const backgroundVisualSeed =
|
||||||
`${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
|
performanceMode === 'lite'
|
||||||
|
? ''
|
||||||
|
: `${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
|
||||||
questionPlan.question?.id ?? ''
|
questionPlan.question?.id ?? ''
|
||||||
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
|
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
|
||||||
rejectedPostIds.size
|
rejectedPostIds.size
|
||||||
@@ -3446,6 +3617,7 @@ 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,
|
||||||
@@ -3471,6 +3643,7 @@ 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,
|
||||||
@@ -3494,6 +3667,7 @@ 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 ({
|
||||||
@@ -3561,6 +3735,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
}, [
|
}, [
|
||||||
posts,
|
posts,
|
||||||
gameSeed,
|
gameSeed,
|
||||||
|
performanceMode,
|
||||||
materialIndex,
|
materialIndex,
|
||||||
acceptedQuestions,
|
acceptedQuestions,
|
||||||
acceptedQuestionMatchIndex,
|
acceptedQuestionMatchIndex,
|
||||||
@@ -3631,6 +3806,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
gameSeed,
|
gameSeed,
|
||||||
recentFirstQuestionPenaltyById,
|
recentFirstQuestionPenaltyById,
|
||||||
userPriorWeights,
|
userPriorWeights,
|
||||||
|
performanceMode,
|
||||||
materialIndex,
|
materialIndex,
|
||||||
matchIndex: acceptedQuestionMatchIndex,
|
matchIndex: acceptedQuestionMatchIndex,
|
||||||
lastGuessQuestionCount,
|
lastGuessQuestionCount,
|
||||||
@@ -3664,6 +3840,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
gameSeed,
|
gameSeed,
|
||||||
recentFirstQuestionPenaltyById,
|
recentFirstQuestionPenaltyById,
|
||||||
userPriorWeights,
|
userPriorWeights,
|
||||||
|
performanceMode,
|
||||||
materialIndex,
|
materialIndex,
|
||||||
matchIndex: acceptedQuestionMatchIndex,
|
matchIndex: acceptedQuestionMatchIndex,
|
||||||
lastGuessQuestionCount,
|
lastGuessQuestionCount,
|
||||||
@@ -3894,6 +4071,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
gameSeed,
|
gameSeed,
|
||||||
recentFirstQuestionPenaltyById,
|
recentFirstQuestionPenaltyById,
|
||||||
userPriorWeights,
|
userPriorWeights,
|
||||||
|
performanceMode,
|
||||||
materialIndex,
|
materialIndex,
|
||||||
matchIndex: acceptedQuestionMatchIndex,
|
matchIndex: acceptedQuestionMatchIndex,
|
||||||
lastGuessQuestionCount,
|
lastGuessQuestionCount,
|
||||||
@@ -4108,15 +4286,16 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
|
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<GekanatorBackdrop
|
{performanceMode !== 'lite' && (
|
||||||
posts={backgroundPosts}
|
<GekanatorBackdrop
|
||||||
mascotAsset={mascotAsset}
|
posts={backgroundPosts}
|
||||||
phase={phase}
|
mascotAsset={mascotAsset}
|
||||||
displayedGuess={displayedGuess}
|
phase={phase}
|
||||||
visualSeed={backgroundVisualSeed}
|
displayedGuess={displayedGuess}
|
||||||
motionMode={effectiveBackgroundMotionMode}
|
visualSeed={backgroundVisualSeed}
|
||||||
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
|
motionMode={effectiveBackgroundMotionMode}
|
||||||
winningRunQuestionCount={winningRunQuestionsAsked}/>
|
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
|
||||||
|
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">
|
||||||
@@ -4126,31 +4305,32 @@ 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">
|
||||||
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
|
{performanceMode === 'normal' && (
|
||||||
text-xs shadow-sm backdrop-blur dark:border-red-800
|
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
|
||||||
dark:bg-red-950/75">
|
text-xs shadow-sm backdrop-blur dark:border-red-800
|
||||||
<span className="mr-2 font-bold text-neutral-600 dark:text-neutral-300">
|
dark:bg-red-950/75">
|
||||||
背景
|
<span className="mr-2 font-bold text-neutral-600 dark:text-neutral-300">
|
||||||
</span>
|
背景
|
||||||
{[{ mode: 'off' as const, label: 'オフ' },
|
</span>
|
||||||
{ mode: 'on' as const, label: 'オン' }]
|
{[{ mode: 'off' as const, label: 'オフ' },
|
||||||
.map (({ mode, label }) => (
|
{ mode: 'on' as const, label: 'オン' }]
|
||||||
<button
|
.map (({ mode, label }) => (
|
||||||
key={mode}
|
<button
|
||||||
type="button"
|
key={mode}
|
||||||
className={cn (
|
type="button"
|
||||||
'rounded-full px-2.5 py-1 transition-colors',
|
className={cn (
|
||||||
backgroundMotionMode === mode
|
'rounded-full px-2.5 py-1 transition-colors',
|
||||||
? 'bg-pink-600 text-white'
|
backgroundMotionMode === mode
|
||||||
: 'text-neutral-600 hover:bg-yellow-100 dark:text-neutral-300 dark:hover:bg-red-900')}
|
? 'bg-pink-600 text-white'
|
||||||
onClick={() => setBackgroundMotionMode (mode)}>
|
: 'text-neutral-600 hover:bg-yellow-100 dark:text-neutral-300 dark:hover:bg-red-900')}
|
||||||
{label}
|
onClick={() => setBackgroundMotionMode (mode)}>
|
||||||
</button>))}
|
{label}
|
||||||
{prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && (
|
</button>))}
|
||||||
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
|
{prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && (
|
||||||
端末設定により控えめ表示
|
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||||
</span>)}
|
端末設定により控えめ表示
|
||||||
</div>
|
</span>)}
|
||||||
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -4428,7 +4608,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 && (
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする