このコミットが含まれているのは:
2026-06-14 05:17:13 +09:00
コミット 6750692445
9個のファイルの変更88行の追加114行の削除
バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 559 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 146 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 1.2 MiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 188 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 201 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 196 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 179 KiB

+2 -2
ファイルの表示
@@ -66,8 +66,8 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg }, { name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] }, { name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] },
{ name: 'おたのしみ', visible: false, subMenu: [ { name: 'おたのしみ', visible: false, subMenu: [
{ name: 'グカネータ', to: '/gekanator' }, { name: '上映会 (β)', to: '/theatres/1' },
{ name: '上映会 (β)', to: '/theatres/1' }] }, { name: 'グカネータ (β)', to: '/gekanator' }] },
{ name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [ { name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [
{ name: '一覧', to: '/users', visible: false }, { name: '一覧', to: '/users', visible: false },
{ name: 'お前', to: `/users/${ user?.id }`, visible: false }, { name: 'お前', to: `/users/${ user?.id }`, visible: false },
+86 -112
ファイルの表示
@@ -790,7 +790,7 @@ const indexedQuestionTextForTag = (key: string): string => {
case 'material': case 'material':
return `素材「${ label }」に関係している?` return `素材「${ label }」に関係している?`
case 'nico': case 'nico':
return `ニコニコに「${ label }」といタグがついている?` return `ニコニコに「${ label }」といタグがついている?`
default: default:
return `${ label }」が含まれる?` return `${ label }」が含まれる?`
} }
@@ -1260,14 +1260,16 @@ const candidatePostsForState = ({
}) })
} }
const allConcreteAnswerOptionsExhaustedForQuestion = ({ const hasDiscriminatingHardSplitForQuestion = ({
candidateIds, candidateIds,
question, question,
posts,
materialIndex, materialIndex,
matchIndex, matchIndex,
}: { }: {
candidateIds: number[] candidateIds: number[]
question: GekanatorQuestion | null question: GekanatorQuestion | null
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex matchIndex: GekanatorMatchIndex
}): boolean => { }): boolean => {
@@ -1275,17 +1277,16 @@ const allConcreteAnswerOptionsExhaustedForQuestion = ({
return false return false
const dynamicMatchIndex = new Map<string, Set<number>> () const dynamicMatchIndex = new Map<string, Set<number>> ()
const posts = [...materialIndex.postById.values ()] const yesCount = matchingPostCountInIds ({
candidateIds,
posts,
materialIndex,
matchIndex,
question,
dynamicMatchIndex })
const noCount = candidateIds.length - yesCount
return (['yes', 'no'] as GekanatorAnswerValue[]).every (answer => return yesCount > 0 && noCount > 0
postIdsForHardAnswer ({
candidateIds,
question,
answer,
posts,
materialIndex,
matchIndex,
dynamicMatchIndex }).length === 0)
} }
@@ -2266,6 +2267,9 @@ const chooseFallbackQuestion = ({
materialIndex: GekanatorQuestionMaterialIndex materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex matchIndex: GekanatorMatchIndex
}): GekanatorQuestion | null => { }): GekanatorQuestion | null => {
if (posts.length === 0)
return null
const fallbackPosts = posts const fallbackPosts = 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)
@@ -2291,25 +2295,19 @@ const chooseFallbackQuestion = ({
question, question,
dynamicMatchIndex }) dynamicMatchIndex })
const noCount = candidateIds.length - yesCount const noCount = candidateIds.length - yesCount
const knownCount = yesCount + noCount if (yesCount === 0 || noCount === 0)
if (knownCount === 0)
return null return null
return { return {
question, question,
hasSplit: yesCount > 0 && noCount > 0, knownCount: candidateIds.length,
knownCount,
balance: Math.abs (yesCount - noCount) } balance: Math.abs (yesCount - noCount) }
}) })
.filter ((item): item is { .filter ((item): item is {
question: GekanatorQuestion question: GekanatorQuestion
hasSplit: boolean
knownCount: number knownCount: number
balance: number } => item !== null) balance: number } => item !== null)
.sort ((a, b) => { .sort ((a, b) => {
if (a.hasSplit !== b.hasSplit)
return a.hasSplit ? -1 : 1
if (a.balance !== b.balance) if (a.balance !== b.balance)
return a.balance - b.balance return a.balance - b.balance
@@ -2421,7 +2419,6 @@ const nextQuestionPlanFor = (
winningRunStartAnswerCount } winningRunStartAnswerCount }
} }
const nextQuestionsSinceLastGuess = answers.length - lastGuessQuestionCount
const nextWinningRunTargetId = const nextWinningRunTargetId =
eligiblePosts.length === 1 eligiblePosts.length === 1
? eligiblePosts[0]?.id ?? null ? eligiblePosts[0]?.id ?? null
@@ -2454,14 +2451,6 @@ const nextQuestionPlanFor = (
&& winningRunQuestionCount ( && winningRunQuestionCount (
answers, answers,
nextWinningRunStartAnswerCount) >= winningRunQuestionLimit nextWinningRunStartAnswerCount) >= winningRunQuestionLimit
if (answers.length >= hardMaxQuestions)
return {
question: null,
guess: bestPost (eligiblePosts, scores),
guessReason: 'hard_max_questions',
questionMode: null,
winningRunTargetId: nextWinningRunTargetId,
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
if (winningRunFinished) if (winningRunFinished)
return { return {
question: null, question: null,
@@ -2504,9 +2493,7 @@ const nextQuestionPlanFor = (
} }
const evaluationPosts = const evaluationPosts =
nextQuestionsSinceLastGuess >= minQuestionsBeforeCertainGuess eligiblePosts
? eligiblePosts
: availablePosts
const evaluationQuestions = buildQuestionsForPosts (evaluationPosts) const evaluationQuestions = buildQuestionsForPosts (evaluationPosts)
const normalQuestion = chooseQuestion ({ const normalQuestion = chooseQuestion ({
@@ -2523,7 +2510,7 @@ const nextQuestionPlanFor = (
matchIndex }) matchIndex })
const fallbackQuestion = normalQuestion ?? chooseFallbackQuestion ({ const fallbackQuestion = normalQuestion ?? chooseFallbackQuestion ({
posts: evaluationPosts.length > 0 ? evaluationPosts : availablePosts, posts: evaluationPosts,
allPosts: posts, allPosts: posts,
questions: evaluationQuestions, questions: evaluationQuestions,
answers, answers,
@@ -2545,14 +2532,8 @@ const nextQuestionPlanFor = (
return { return {
question: null, question: null,
guess: guess: null,
answers.length >= hardMaxQuestions guessReason: null,
? bestPost (guessablePosts, scores)
: null,
guessReason:
answers.length >= hardMaxQuestions
? 'hard_max_questions'
: null,
questionMode: null, questionMode: null,
winningRunTargetId: nextWinningRunTargetId, winningRunTargetId: nextWinningRunTargetId,
winningRunStartAnswerCount: nextWinningRunStartAnswerCount } winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
@@ -2595,12 +2576,17 @@ const mascotStateFor = (
bestConfidencePercent: number, bestConfidencePercent: number,
winningRunActive: boolean, winningRunActive: boolean,
): MascotState => { ): MascotState => {
if (phase === 'learned' && resultWon === true) const resultPhase =
return 'celebrate' phase === 'end'
|| phase === 'review'
|| phase === 'learned'
if (phase === 'learned' && resultWon === false) if (resultPhase && !(resultWon))
return 'failed' return 'failed'
if (resultPhase && resultWon)
return 'celebrate'
switch (phase) switch (phase)
{ {
case 'question': case 'question':
@@ -2660,21 +2646,20 @@ const backgroundPostsFor = ({
const GekanatorBackdrop: FC<{ const GekanatorBackdrop: FC<{
posts: Post[] posts: Post[]
mascotAsset: string
phase: Phase phase: Phase
displayedGuess?: Post | null displayedGuess?: Post | null
visualSeed: string visualSeed: string
motionMode: BackgroundMotionMode motionMode: BackgroundMotionMode
winningRunTargetPost?: Post | null winningRunTargetPost?: Post | null
winningRunQuestionCount?: number winningRunQuestionCount?: number }> = ({ posts,
}> = ({ mascotAsset,
posts, phase,
phase, displayedGuess = null,
displayedGuess = null, visualSeed,
visualSeed, motionMode,
motionMode, winningRunTargetPost = null,
winningRunTargetPost = null, winningRunQuestionCount = 0 }) => {
winningRunQuestionCount = 0,
}) => {
const guessFocusOffset = useMemo (() => { const guessFocusOffset = useMemo (() => {
const focusTiles = [ const focusTiles = [
{ x: 'calc(max(100vw, 100vh) * 0.5)', { x: 'calc(max(100vw, 100vh) * 0.5)',
@@ -2996,7 +2981,7 @@ const GekanatorBackdrop: FC<{
to-pink-50 dark:from-red-950 dark:via-red-975 dark:to-red-900"/>) to-pink-50 dark:from-red-950 dark:via-red-975 dark:to-red-900"/>)
return ( return (
<div className="absolute inset-0 overflow-hidden pointer-events-none"> <div className="fixed [inset:48px_0_0_0] z-0 overflow-hidden pointer-events-none">
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<motion.div <motion.div
className="relative shrink-0" className="relative shrink-0"
@@ -3060,13 +3045,16 @@ const GekanatorBackdrop: FC<{
layout={displayedBackdropMode !== 'normal'} layout={displayedBackdropMode !== 'normal'}
transition={{ duration: tileFlipDuration, ease: 'easeInOut' }} transition={{ duration: tileFlipDuration, ease: 'easeInOut' }}
style={{ perspective: 1600 }}> style={{ perspective: 1600 }}>
{displayedBackdropMode !== 'normal' || !(isFlippingTiles) ? ( {(displayedBackdropMode !== 'normal' || !(isFlippingTiles))
? (
<img <img
src={thumbnail} src={['intro', 'end'].includes (phase)
? mascotAsset
: thumbnail}
alt="" alt=""
className="absolute inset-0 h-full w-full object-cover" className="absolute inset-0 h-full w-full object-cover"
style={{ opacity: renderedSettings.opacity }}/> style={{ opacity: renderedSettings.opacity }}/>)
) : ( : (
<motion.div <motion.div
className="absolute inset-0" className="absolute inset-0"
initial={{ rotateY: 0 }} initial={{ rotateY: 0 }}
@@ -3098,7 +3086,7 @@ const GekanatorBackdrop: FC<{
</motion.div> </motion.div>
</motion.div> </motion.div>
</div> </div>
<div className="absolute inset-0 bg-gradient-to-br from-yellow-50/76 via-white/58 <div className="fixed inset-0 z-0 bg-gradient-to-br from-yellow-50/76 via-white/58
to-pink-100/62 dark:from-red-950/78 dark:via-red-975/60 to-pink-100/62 dark:from-red-950/78 dark:via-red-975/60
dark:to-red-900/66"/> dark:to-red-900/66"/>
</div>) </div>)
@@ -3503,12 +3491,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${ }:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
rejectedPostIds.size rejectedPostIds.size
}:${ backgroundPosts.slice (0, 8).map (post => post.id).join ('|') }` }:${ backgroundPosts.slice (0, 8).map (post => post.id).join ('|') }`
const mascot = mascotStateFor ( const mascot = mascotStateFor (phase, effectiveResultWon, eligiblePosts.length,
phase, bestConfidencePercent, winningRunActive)
effectiveResultWon,
eligiblePosts.length,
bestConfidencePercent,
winningRunActive)
const mascotAsset = mascotAssetByState[mascot] const mascotAsset = mascotAssetByState[mascot]
const mascotAlt = mascotAltByState[mascot] const mascotAlt = mascotAltByState[mascot]
const saveMutation = useMutation ({ const saveMutation = useMutation ({
@@ -3535,7 +3519,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
onSuccess: async () => { onSuccess: async () => {
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () }) await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () })
setExtraQuestionState ('saved') setExtraQuestionState ('saved')
setPhase ('learned') setPhase ('end')
}}) }})
const resetExtraQuestionState = () => { const resetExtraQuestionState = () => {
@@ -3697,11 +3681,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
matchIndex: acceptedQuestionMatchIndex }) matchIndex: acceptedQuestionMatchIndex })
return !(fallbackQuestion) return !(fallbackQuestion)
|| allConcreteAnswerOptionsExhaustedForQuestion ({ || !(hasDiscriminatingHardSplitForQuestion ({
candidateIds: recoveredEligiblePosts.map (post => post.id), candidateIds: recoveredEligiblePosts.map (post => post.id),
question: fallbackQuestion, question: fallbackQuestion,
posts,
materialIndex, materialIndex,
matchIndex: acceptedQuestionMatchIndex }) matchIndex: acceptedQuestionMatchIndex }))
} }
while (recoveredEligiblePosts.length === 0 || needsPreQuestionRecovery ()) while (recoveredEligiblePosts.length === 0 || needsPreQuestionRecovery ())
@@ -3962,12 +3947,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const saveAndLearn = () => { const saveAndLearn = () => {
if (!(canPersistGame)) if (!(canPersistGame))
{ {
setPhase ('learned') setPhase ('end')
return return
} }
resetExtraQuestionState () resetExtraQuestionState ()
saveReviewedResult (() => setPhase ('learned')) saveReviewedResult (() => setPhase ('end'))
} }
const submitQuestionSuggestion = () => { const submitQuestionSuggestion = () => {
@@ -4197,10 +4182,19 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
[String (questionId)]: value }) [String (questionId)]: value })
} }
const dialogue = const introDialogue =
phase === 'learned' && resultWon <><ruby>鹿<rt></rt></ruby>稿</>
? <>wwwww&emsp;<ruby>鹿<rt></rt></ruby>!</>
: <><ruby>鹿<rt></rt></ruby>稿</> const winDialogue =
<>wwwww&emsp;<ruby>鹿<rt></rt></ruby>!</>
const loseDialogue =
<>!&emsp;<ruby>鹿<rt></rt></ruby>!!!!!</>
const resultDialogue = effectiveResultWon ? winDialogue : loseDialogue
const dialogue = phase === 'learned' ? resultDialogue : introDialogue
const introLoading = isLoading || acceptedQuestionsLoading const introLoading = isLoading || acceptedQuestionsLoading
const readyToStart = const readyToStart =
!(introLoading) !(introLoading)
@@ -4287,7 +4281,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
acceptedQuestionsLoading]) acceptedQuestionsLoading])
return ( return (
<MainArea className="relative overflow-hidden bg-yellow-50 dark:bg-red-975"> <MainArea className="relative isolate overflow-x-hidden bg-yellow-50 dark:bg-red-975">
<Helmet> <Helmet>
<title>{`グカネータ | ${ SITE_TITLE }`}</title> <title>{`グカネータ | ${ SITE_TITLE }`}</title>
</Helmet> </Helmet>
@@ -4295,6 +4289,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{performanceMode !== 'lite' && ( {performanceMode !== 'lite' && (
<GekanatorBackdrop <GekanatorBackdrop
posts={backgroundPosts} posts={backgroundPosts}
mascotAsset={mascotAsset}
phase={phase} phase={phase}
displayedGuess={displayedGuess} displayedGuess={displayedGuess}
visualSeed={backgroundVisualSeed} visualSeed={backgroundVisualSeed}
@@ -4367,14 +4362,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</p>)} </p>)}
{(Boolean (error) || Boolean (acceptedQuestionsError)) {(Boolean (error) || Boolean (acceptedQuestionsError))
&& <p></p>} && <p></p>}
{!(canPersistGame) && (
<p className="text-sm text-neutral-600 dark:text-neutral-300">
<PrefetchLink to="/users/settings" className="ml-1 underline">
</PrefetchLink>
</p>)}
{phase === 'intro' && readyToStart && restorePromptVisible && ( {phase === 'intro' && readyToStart && restorePromptVisible && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@@ -4509,7 +4496,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</div>)} </div>)}
{phase === 'guess' && displayedGuess && ( {phase === 'guess' && displayedGuess && (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-xl font-bold">?</p> <p className="text-xl font-bold">?</p>
{isAdmin && ( {isAdmin && (
<div className="rounded border border-yellow-100 px-3 py-2 <div className="rounded border border-yellow-100 px-3 py-2
text-sm dark:border-red-900"> text-sm dark:border-red-900">
@@ -4595,8 +4582,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{phase === 'end' && ( {phase === 'end' && (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<p className="text-sm text-neutral-500"></p> <p className="text-xl font-bold">{resultDialogue}</p>
<p className="text-xl font-bold">wwwww&emsp;<ruby>鹿<rt></rt></ruby>!</p>
</div> </div>
{reviewGuessedPost && ( {reviewGuessedPost && (
@@ -4682,7 +4668,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|| extraQuestionState === 'loading' || extraQuestionState === 'loading'
|| extraQuestionAnswersMutation.isPending} || extraQuestionAnswersMutation.isPending}
onClick={startExtraQuestions}> onClick={startExtraQuestions}>
</button> </button>
</div> </div>
</div>)} </div>)}
@@ -4690,8 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{phase === 'review' && ( {phase === 'review' && (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<p className="text-sm text-neutral-500"></p> <p className="text-xl font-bold"></p>
<p className="text-xl font-bold"></p>
</div> </div>
{reviewGuessedPost && ( {reviewGuessedPost && (
@@ -4766,7 +4751,8 @@ 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 && (
@@ -4775,6 +4761,14 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</p>)} </p>)}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button
type="button"
className="rounded border border-neutral-300 px-4 py-2
hover:bg-neutral-100 dark:border-neutral-700
dark:hover:bg-red-900"
onClick={() => setPhase ('end')}>
</button>
<button <button
type="button" type="button"
className="rounded bg-pink-600 px-4 py-2 font-bold text-white className="rounded bg-pink-600 px-4 py-2 font-bold text-white
@@ -4788,14 +4782,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
onClick={saveAndLearn}> onClick={saveAndLearn}>
</button> </button>
<button
type="button"
className="rounded border border-neutral-300 px-4 py-2
hover:bg-neutral-100 dark:border-neutral-700
dark:hover:bg-red-900"
onClick={() => setPhase ('end')}>
</button>
</div> </div>
</div>)} </div>)}
@@ -4945,7 +4931,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
question => !(extraQuestionAnswers[String (question.id)])) question => !(extraQuestionAnswers[String (question.id)]))
} }
onClick={saveExtraQuestions}> onClick={saveExtraQuestions}>
</button> </button>
</div> </div>
{!(canPersistGame) && ( {!(canPersistGame) && (
@@ -4953,18 +4939,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</p>)} </p>)}
</div>)} </div>)}
{phase === 'learned' && (
<div className="space-y-3">
<p>{extraQuestionState === 'saved' ? '学習しました。' : '覚えたよ.次はもっと見通す.'}</p>
<button
type="button"
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
hover:bg-pink-500"
onClick={reset}>
</button>
</div>)}
</div> </div>
</div> </div>
</section> </section>