From 0d7757b2dfbffc1fcf3cd8f98e187851272b92ea Mon Sep 17 00:00:00 2001 From: miteruzo Date: Mon, 15 Jun 2026 22:09:10 +0900 Subject: [PATCH] #370 --- frontend/src/lib/gekanator.ts | 2 +- frontend/src/pages/GekanatorPage.tsx | 366 +++++++-------------------- 2 files changed, 94 insertions(+), 274 deletions(-) diff --git a/frontend/src/lib/gekanator.ts b/frontend/src/lib/gekanator.ts index f6a5b7e..0604c8c 100644 --- a/frontend/src/lib/gekanator.ts +++ b/frontend/src/lib/gekanator.ts @@ -30,7 +30,7 @@ export type GekanatorQuestionSource = | 'ai_generated' | 'admin_curated' -export type GekanatorPerformanceMode = 'lite' | 'normal' +export type GekanatorPerformanceMode = 'normal' export type GekanatorQuestionCondition = | { type: 'tag'; key: string } diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 5522e36..3c59931 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -29,7 +29,6 @@ import type { FC } from 'react' import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorExtraQuestion, - GekanatorPerformanceMode, GekanatorQuestionCondition, GekanatorQuestionKind, GekanatorQuestion, @@ -162,7 +161,6 @@ const confidenceTemperature = 6 const gameStorageKey = 'gekanator:game:v1' const recentGamesStorageKey = 'gekanator:recent-games:v1' const backgroundMotionStorageKey = 'gekanator:background-motion:v1' -const performanceModeStorageKey = 'gekanator:performance-mode:v1' const maxQuestionSuggestionsPerGame = 3 const maxStoredRecentGames = 12 const mascotAssetByState: Record = { @@ -391,14 +389,8 @@ const storeRecentGameSummary = ( } -const loadBackgroundMotionMode = ( - performanceMode?: GekanatorPerformanceMode, -): BackgroundMotionMode => { - const fallbackMode = - performanceMode === 'lite' ? 'off' - : performanceMode === 'normal' ? 'on' - : detectDefaultPerformanceMode () === 'lite' ? 'off' - : 'on' +const loadBackgroundMotionMode = (): BackgroundMotionMode => { + const fallbackMode = 'on' try { 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 = (): { extraQuestions: GekanatorExtraQuestion[] extraQuestionAnswers: Record @@ -1055,11 +1015,9 @@ const rankedEntriesForCounts = ( const buildQuestionsForCandidateIds = ( { candidateIds, materialIndex, - performanceMode, acceptedQuestions }: { candidateIds: number[] - materialIndex: GekanatorQuestionMaterialIndex - performanceMode: GekanatorPerformanceMode - acceptedQuestions: GekanatorQuestion[] }, + materialIndex: GekanatorQuestionMaterialIndex + acceptedQuestions: GekanatorQuestion[] }, ): GekanatorQuestion[] => { const total = candidateIds.length if (total === 0) @@ -1089,23 +1047,16 @@ const buildQuestionsForCandidateIds = ( const monthDay = materialIndex.originalMonthDayByPostId.get (postId) if (monthDay) monthDayCounts.set (monthDay, (monthDayCounts.get (monthDay) ?? 0) + 1) - if (performanceMode === 'normal') - materialIndex.titleTermsByPostId.get (postId)?.forEach (term => - 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 titleLengths.push (titleLength) if (materialIndex.titleAsciiPostIds.has (postId)) ++asciiCount }) - const tagCap = - performanceMode === 'lite' - ? total >= 80 ? 96 : 64 - : total >= 120 ? 128 : 96 - const titleTermCap = - performanceMode === 'lite' - ? 0 - : total >= 80 ? 10 : total >= 24 ? 14 : 20 + const tagCap = total >= 120 ? 128 : 96 + const titleTermCap = total >= 80 ? 10 : total >= 24 ? 14 : 20 const factCap = total >= 80 ? 8 : 12 const sortedLengths = [...titleLengths].sort ((a, b) => a - b) const titleLengthMedian = sortedLengths[Math.floor (sortedLengths.length / 2)] ?? 0 @@ -1181,17 +1132,16 @@ const buildQuestionsForCandidateIds = ( materialIndex })) }) - if (performanceMode === 'normal') - rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap }) - .filter (([term]) => String (term).length <= 24) - .forEach (([term]) => { - questions.push (buildIndexedQuestion ({ - condition: { type: 'title-contains', text: String (term) }, - text: `題名に「${ term }」が含まれる?`, - kind: 'title', - priorityWeight: .96, - materialIndex })) - }) + rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap }) + .filter (([term]) => String (term).length <= 24) + .forEach (([term]) => { + questions.push (buildIndexedQuestion ({ + condition: { type: 'title-contains', text: String (term) }, + text: `題名に「${ term }」が含まれる?`, + kind: 'title', + priorityWeight: .96, + materialIndex })) + }) return mergeQuestions ([...questions, ...acceptedQuestions]) } @@ -1615,41 +1565,33 @@ const contradictionPenaltyFor = ({ } -const chooseQuestion = ({ - posts, - questions, - scores, - answers, - askedIds, - gameSeed, - recentFirstQuestionPenaltyById, - userPriorWeights, - performanceMode, - materialIndex, - matchIndex, -}: { - posts: Post[] - questions: GekanatorQuestion[] - scores: Map - answers: GekanatorAnswerLog[] - askedIds: Set - gameSeed: string - recentFirstQuestionPenaltyById: Map - userPriorWeights: Map - performanceMode: GekanatorPerformanceMode - materialIndex: GekanatorQuestionMaterialIndex - matchIndex: GekanatorMatchIndex -}): GekanatorQuestion | null => { - const candidateIds = posts.map (post => post.id) - const candidateIdSet = new Set (candidateIds) +const chooseQuestion = ( + { posts, + questions, + scores, + answers, + askedIds, + gameSeed, + recentFirstQuestionPenaltyById, + userPriorWeights, + materialIndex, + matchIndex }: { posts: Post[] + questions: GekanatorQuestion[] + scores: Map + answers: GekanatorAnswerLog[] + askedIds: Set + gameSeed: string + recentFirstQuestionPenaltyById: Map + userPriorWeights: Map + materialIndex: GekanatorQuestionMaterialIndex + matchIndex: GekanatorMatchIndex }, +): GekanatorQuestion | null => { const dynamicMatchIndex = new Map> () const invertedSignature = (signature: string): string => signature.replace (/[01]/g, value => value === '1' ? '0' : '1') - const redundantSignatures = ( - candidates: Post[], - ): Set => { + const redundantSignatures = (candidates: Post[]): Set => { const signatures = new Set () questions .filter (question => askedIds.has (question.id)) @@ -1668,89 +1610,6 @@ const chooseQuestion = ({ 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 .map (post => ({ post, score: scores.get (post.id) ?? 0 })) .sort ((a, b) => b.score - a.score) @@ -2361,7 +2220,6 @@ const nextQuestionPlanFor = ( gameSeed, recentFirstQuestionPenaltyById, userPriorWeights, - performanceMode, materialIndex, matchIndex, lastGuessQuestionCount, @@ -2376,7 +2234,6 @@ const nextQuestionPlanFor = ( gameSeed: string recentFirstQuestionPenaltyById: Map userPriorWeights: Map - performanceMode: GekanatorPerformanceMode materialIndex: GekanatorQuestionMaterialIndex matchIndex: GekanatorMatchIndex lastGuessQuestionCount: number @@ -2439,7 +2296,6 @@ const nextQuestionPlanFor = ( buildQuestionsForCandidateIds ({ candidateIds: scopePosts.map (post => post.id), materialIndex, - performanceMode, acceptedQuestions }) if (eligiblePosts.length === 1) @@ -2505,7 +2361,6 @@ const nextQuestionPlanFor = ( gameSeed, recentFirstQuestionPenaltyById, userPriorWeights, - performanceMode, materialIndex, matchIndex }) @@ -3108,10 +2963,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const canPersistGame = user !== null const [recentGames, setRecentGames] = useState ( () => loadRecentGames ()) - const [performanceMode] = - useState (() => loadPerformanceMode ()) const [backgroundMotionMode, setBackgroundMotionMode] = useState ( - () => loadBackgroundMotionMode (loadPerformanceMode ())) + () => loadBackgroundMotionMode ()) const [prefersReducedMotion, setPrefersReducedMotion] = useState (false) const [gameSeed, setGameSeed] = useState ( storedGame?.gameSeed ?? createGameSeed ()) @@ -3328,17 +3181,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { } }, [backgroundMotionMode]) - useEffect (() => { - try - { - localStorage.setItem (performanceModeStorageKey, performanceMode) - } - catch - { - return - } - }, [performanceMode]) - const askedQuestionById = useMemo ( () => new Map (askedQuestionBank.map (question => [question.id, question])), [askedQuestionBank]) @@ -3361,9 +3203,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { () => new Map (scoringQuestions.map (question => [question.id, question])), [scoringQuestions]) const recentFirstQuestionPenaltyById = useMemo (() => { - if (performanceMode === 'lite') - return new Map () - const penalties = new Map () recentGames.forEach ((game, index) => { @@ -3376,12 +3215,10 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { }) return penalties - }, [performanceMode, recentGames]) + }, [recentGames]) const userPriorWeights = useMemo ( - () => performanceMode === 'lite' - ? new Map () - : userPriorWeightsFor (posts, recentGames), - [performanceMode, posts, recentGames]) + () => userPriorWeightsFor (posts, recentGames), + [posts, recentGames]) const availablePosts = useMemo ( () => posts.filter (post => !(rejectedPostIds.has (post.id))), [posts, rejectedPostIds]) @@ -3397,7 +3234,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { gameSeed, recentFirstQuestionPenaltyById, userPriorWeights, - performanceMode, materialIndex, matchIndex: acceptedQuestionMatchIndex, lastGuessQuestionCount, @@ -3405,7 +3241,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { winningRunStartAnswerCount }), [posts, eligiblePosts, availablePosts, acceptedQuestions, scores, answers, askedIds, gameSeed, recentFirstQuestionPenaltyById, - userPriorWeights, performanceMode, materialIndex, acceptedQuestionMatchIndex, + userPriorWeights, materialIndex, acceptedQuestionMatchIndex, lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount]) const winningRunTargetPost = useMemo ( () => questionPlan.winningRunTargetId === null @@ -3458,35 +3294,28 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const reviewCorrectPost = posts.find (post => post.id === reviewCorrectPostId) ?? null const effectiveResultWon = - resultWon ?? ( - reviewGuessedPostId !== null - && reviewCorrectPostId !== null - ? reviewGuessedPostId === reviewCorrectPostId - : null) + resultWon + ?? ((reviewGuessedPostId !== null && reviewCorrectPostId !== null) + ? reviewGuessedPostId === reviewCorrectPostId + : null) const effectiveBackgroundMotionMode = - performanceMode === 'lite' - ? 'off' - : backgroundMotionMode === 'off' - ? 'off' - : prefersReducedMotion - ? 'calm' - : backgroundMotionMode + backgroundMotionMode === 'off' + ? 'off' + : (prefersReducedMotion + ? 'calm' + : backgroundMotionMode) const backgroundPosts = useMemo ( - () => performanceMode === 'lite' - ? [] - : backgroundPostsFor ({ + () => backgroundPostsFor ({ phase, eligiblePosts, availablePosts, displayedGuess, reviewCorrectPost, reviewGuessedPost }), - [performanceMode, phase, eligiblePosts, availablePosts, displayedGuess, - reviewCorrectPost, reviewGuessedPost]) + [phase, eligiblePosts, availablePosts, displayedGuess, reviewCorrectPost, + reviewGuessedPost]) const backgroundVisualSeed = - performanceMode === 'lite' - ? '' - : `${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${ + `${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${ questionPlan.question?.id ?? '' }:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${ rejectedPostIds.size @@ -3617,7 +3446,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { let recoveredQuestions = buildQuestionsForCandidateIds ({ candidateIds: recoveredEligiblePosts.map (post => post.id), materialIndex, - performanceMode, acceptedQuestions }) let recoveredScoringQuestions = mergeQuestions ([ ...recoveredQuestions, @@ -3643,7 +3471,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { recoveredQuestions = buildQuestionsForCandidateIds ({ candidateIds: recoveredEligiblePosts.map (post => post.id), materialIndex, - performanceMode, acceptedQuestions }) recoveredScoringQuestions = mergeQuestions ([ ...recoveredQuestions, @@ -3667,7 +3494,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { gameSeed, recentFirstQuestionPenaltyById, userPriorWeights, - performanceMode, materialIndex, matchIndex: acceptedQuestionMatchIndex }) const fallbackQuestion = nextQuestion ?? chooseFallbackQuestion ({ @@ -3735,7 +3561,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { }, [ posts, gameSeed, - performanceMode, materialIndex, acceptedQuestions, acceptedQuestionMatchIndex, @@ -3806,7 +3631,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { gameSeed, recentFirstQuestionPenaltyById, userPriorWeights, - performanceMode, materialIndex, matchIndex: acceptedQuestionMatchIndex, lastGuessQuestionCount, @@ -3840,7 +3664,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { gameSeed, recentFirstQuestionPenaltyById, userPriorWeights, - performanceMode, materialIndex, matchIndex: acceptedQuestionMatchIndex, lastGuessQuestionCount, @@ -4071,7 +3894,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { gameSeed, recentFirstQuestionPenaltyById, userPriorWeights, - performanceMode, materialIndex, matchIndex: acceptedQuestionMatchIndex, lastGuessQuestionCount, @@ -4286,16 +4108,15 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {`グカネータ | ${ SITE_TITLE }`} - {performanceMode !== 'lite' && ( - )} +
@@ -4305,32 +4126,31 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
- {performanceMode === 'normal' && ( -
- - 背景 - - {[{ mode: 'off' as const, label: 'オフ' }, - { mode: 'on' as const, label: 'オン' }] - .map (({ mode, label }) => ( - ))} - {prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && ( - - 端末設定により控えめ表示 - )} -
)} +
+ + 背景 + + {[{ mode: 'off' as const, label: 'オフ' }, + { mode: 'on' as const, label: 'オン' }] + .map (({ mode, label }) => ( + ))} + {prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && ( + + 端末設定により控えめ表示 + )} +
@@ -4608,7 +4428,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {reviewGuessedPostId !== null && reviewCorrectPostId !== null && (

- 判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'} + 判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}

)} {saveMutation.isError && ( -- 2.34.1