このコミットが含まれているのは:
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/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] },
{ 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', visible: false },
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
+78 -104
ファイルの表示
@@ -790,7 +790,7 @@ const indexedQuestionTextForTag = (key: string): string => {
case 'material':
return `素材「${ label }」に関係している?`
case 'nico':
return `ニコニコに「${ label }」といタグがついている?`
return `ニコニコに「${ label }」といタグがついている?`
default:
return `${ label }」が含まれる?`
}
@@ -1260,14 +1260,16 @@ const candidatePostsForState = ({
})
}
const allConcreteAnswerOptionsExhaustedForQuestion = ({
const hasDiscriminatingHardSplitForQuestion = ({
candidateIds,
question,
posts,
materialIndex,
matchIndex,
}: {
candidateIds: number[]
question: GekanatorQuestion | null
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
}): boolean => {
@@ -1275,17 +1277,16 @@ const allConcreteAnswerOptionsExhaustedForQuestion = ({
return false
const dynamicMatchIndex = new Map<string, Set<number>> ()
const posts = [...materialIndex.postById.values ()]
return (['yes', 'no'] as GekanatorAnswerValue[]).every (answer =>
postIdsForHardAnswer ({
const yesCount = matchingPostCountInIds ({
candidateIds,
question,
answer,
posts,
materialIndex,
matchIndex,
dynamicMatchIndex }).length === 0)
question,
dynamicMatchIndex })
const noCount = candidateIds.length - yesCount
return yesCount > 0 && noCount > 0
}
@@ -2266,6 +2267,9 @@ const chooseFallbackQuestion = ({
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
}): GekanatorQuestion | null => {
if (posts.length === 0)
return null
const fallbackPosts = posts
.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
.sort ((a, b) => b.score - a.score)
@@ -2291,25 +2295,19 @@ const chooseFallbackQuestion = ({
question,
dynamicMatchIndex })
const noCount = candidateIds.length - yesCount
const knownCount = yesCount + noCount
if (knownCount === 0)
if (yesCount === 0 || noCount === 0)
return null
return {
question,
hasSplit: yesCount > 0 && noCount > 0,
knownCount,
knownCount: candidateIds.length,
balance: Math.abs (yesCount - noCount) }
})
.filter ((item): item is {
question: GekanatorQuestion
hasSplit: boolean
knownCount: number
balance: number } => item !== null)
.sort ((a, b) => {
if (a.hasSplit !== b.hasSplit)
return a.hasSplit ? -1 : 1
if (a.balance !== b.balance)
return a.balance - b.balance
@@ -2421,7 +2419,6 @@ const nextQuestionPlanFor = (
winningRunStartAnswerCount }
}
const nextQuestionsSinceLastGuess = answers.length - lastGuessQuestionCount
const nextWinningRunTargetId =
eligiblePosts.length === 1
? eligiblePosts[0]?.id ?? null
@@ -2454,14 +2451,6 @@ const nextQuestionPlanFor = (
&& winningRunQuestionCount (
answers,
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)
return {
question: null,
@@ -2504,9 +2493,7 @@ const nextQuestionPlanFor = (
}
const evaluationPosts =
nextQuestionsSinceLastGuess >= minQuestionsBeforeCertainGuess
? eligiblePosts
: availablePosts
eligiblePosts
const evaluationQuestions = buildQuestionsForPosts (evaluationPosts)
const normalQuestion = chooseQuestion ({
@@ -2523,7 +2510,7 @@ const nextQuestionPlanFor = (
matchIndex })
const fallbackQuestion = normalQuestion ?? chooseFallbackQuestion ({
posts: evaluationPosts.length > 0 ? evaluationPosts : availablePosts,
posts: evaluationPosts,
allPosts: posts,
questions: evaluationQuestions,
answers,
@@ -2545,14 +2532,8 @@ const nextQuestionPlanFor = (
return {
question: null,
guess:
answers.length >= hardMaxQuestions
? bestPost (guessablePosts, scores)
: null,
guessReason:
answers.length >= hardMaxQuestions
? 'hard_max_questions'
: null,
guess: null,
guessReason: null,
questionMode: null,
winningRunTargetId: nextWinningRunTargetId,
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
@@ -2595,12 +2576,17 @@ const mascotStateFor = (
bestConfidencePercent: number,
winningRunActive: boolean,
): MascotState => {
if (phase === 'learned' && resultWon === true)
return 'celebrate'
const resultPhase =
phase === 'end'
|| phase === 'review'
|| phase === 'learned'
if (phase === 'learned' && resultWon === false)
if (resultPhase && !(resultWon))
return 'failed'
if (resultPhase && resultWon)
return 'celebrate'
switch (phase)
{
case 'question':
@@ -2660,21 +2646,20 @@ const backgroundPostsFor = ({
const GekanatorBackdrop: FC<{
posts: Post[]
mascotAsset: string
phase: Phase
displayedGuess?: Post | null
visualSeed: string
motionMode: BackgroundMotionMode
winningRunTargetPost?: Post | null
winningRunQuestionCount?: number
}> = ({
posts,
winningRunQuestionCount?: number }> = ({ posts,
mascotAsset,
phase,
displayedGuess = null,
visualSeed,
motionMode,
winningRunTargetPost = null,
winningRunQuestionCount = 0,
}) => {
winningRunQuestionCount = 0 }) => {
const guessFocusOffset = useMemo (() => {
const focusTiles = [
{ 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"/>)
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">
<motion.div
className="relative shrink-0"
@@ -3060,13 +3045,16 @@ const GekanatorBackdrop: FC<{
layout={displayedBackdropMode !== 'normal'}
transition={{ duration: tileFlipDuration, ease: 'easeInOut' }}
style={{ perspective: 1600 }}>
{displayedBackdropMode !== 'normal' || !(isFlippingTiles) ? (
{(displayedBackdropMode !== 'normal' || !(isFlippingTiles))
? (
<img
src={thumbnail}
src={['intro', 'end'].includes (phase)
? mascotAsset
: thumbnail}
alt=""
className="absolute inset-0 h-full w-full object-cover"
style={{ opacity: renderedSettings.opacity }}/>
) : (
style={{ opacity: renderedSettings.opacity }}/>)
: (
<motion.div
className="absolute inset-0"
initial={{ rotateY: 0 }}
@@ -3098,7 +3086,7 @@ const GekanatorBackdrop: FC<{
</motion.div>
</motion.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
dark:to-red-900/66"/>
</div>)
@@ -3503,12 +3491,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
rejectedPostIds.size
}:${ backgroundPosts.slice (0, 8).map (post => post.id).join ('|') }`
const mascot = mascotStateFor (
phase,
effectiveResultWon,
eligiblePosts.length,
bestConfidencePercent,
winningRunActive)
const mascot = mascotStateFor (phase, effectiveResultWon, eligiblePosts.length,
bestConfidencePercent, winningRunActive)
const mascotAsset = mascotAssetByState[mascot]
const mascotAlt = mascotAltByState[mascot]
const saveMutation = useMutation ({
@@ -3535,7 +3519,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
onSuccess: async () => {
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () })
setExtraQuestionState ('saved')
setPhase ('learned')
setPhase ('end')
}})
const resetExtraQuestionState = () => {
@@ -3697,11 +3681,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
matchIndex: acceptedQuestionMatchIndex })
return !(fallbackQuestion)
|| allConcreteAnswerOptionsExhaustedForQuestion ({
|| !(hasDiscriminatingHardSplitForQuestion ({
candidateIds: recoveredEligiblePosts.map (post => post.id),
question: fallbackQuestion,
posts,
materialIndex,
matchIndex: acceptedQuestionMatchIndex })
matchIndex: acceptedQuestionMatchIndex }))
}
while (recoveredEligiblePosts.length === 0 || needsPreQuestionRecovery ())
@@ -3962,12 +3947,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const saveAndLearn = () => {
if (!(canPersistGame))
{
setPhase ('learned')
setPhase ('end')
return
}
resetExtraQuestionState ()
saveReviewedResult (() => setPhase ('learned'))
saveReviewedResult (() => setPhase ('end'))
}
const submitQuestionSuggestion = () => {
@@ -4197,10 +4182,19 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
[String (questionId)]: value })
}
const dialogue =
phase === 'learned' && resultWon
? <>wwwww&emsp;<ruby>鹿<rt></rt></ruby>!</>
: <><ruby>鹿<rt></rt></ruby>稿</>
const introDialogue =
<><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 readyToStart =
!(introLoading)
@@ -4287,7 +4281,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
acceptedQuestionsLoading])
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>
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
</Helmet>
@@ -4295,6 +4289,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{performanceMode !== 'lite' && (
<GekanatorBackdrop
posts={backgroundPosts}
mascotAsset={mascotAsset}
phase={phase}
displayedGuess={displayedGuess}
visualSeed={backgroundVisualSeed}
@@ -4367,14 +4362,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</p>)}
{(Boolean (error) || Boolean (acceptedQuestionsError))
&& <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 && (
<div className="flex flex-wrap gap-2">
@@ -4509,7 +4496,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</div>)}
{phase === 'guess' && displayedGuess && (
<div className="space-y-4">
<p className="text-xl font-bold">?</p>
<p className="text-xl font-bold">?</p>
{isAdmin && (
<div className="rounded border border-yellow-100 px-3 py-2
text-sm dark:border-red-900">
@@ -4595,8 +4582,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{phase === 'end' && (
<div className="space-y-4">
<div>
<p className="text-sm text-neutral-500"></p>
<p className="text-xl font-bold">wwwww&emsp;<ruby>鹿<rt></rt></ruby>!</p>
<p className="text-xl font-bold">{resultDialogue}</p>
</div>
{reviewGuessedPost && (
@@ -4682,7 +4668,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|| extraQuestionState === 'loading'
|| extraQuestionAnswersMutation.isPending}
onClick={startExtraQuestions}>
</button>
</div>
</div>)}
@@ -4690,8 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{phase === 'review' && (
<div className="space-y-4">
<div>
<p className="text-sm text-neutral-500"></p>
<p className="text-xl font-bold"></p>
<p className="text-xl font-bold"></p>
</div>
{reviewGuessedPost && (
@@ -4766,7 +4751,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
<p className="text-sm text-neutral-600 dark:text-neutral-300">
: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'}
:
{reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
</p>)}
{saveMutation.isError && (
@@ -4775,6 +4761,14 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</p>)}
<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
type="button"
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}>
</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>)}
@@ -4945,7 +4931,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
question => !(extraQuestionAnswers[String (question.id)]))
}
onClick={saveExtraQuestions}>
</button>
</div>
{!(canPersistGame) && (
@@ -4953,18 +4939,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</p>)}
</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>
</section>