このコミットが含まれているのは:
2026-06-16 23:48:29 +09:00
コミット f9f0010e03
3個のファイルの変更200行の追加182行の削除
+48
ファイルの表示
@@ -125,6 +125,54 @@ npm run preview
- TypeScript and TSX use 4-space logical indentation. - TypeScript and TSX use 4-space logical indentation.
- In TypeScript and TSX only, replace every leading run of 8 spaces with a tab. - In TypeScript and TSX only, replace every leading run of 8 spaces with a tab.
- Tabs are only for leading indentation, never for spaces after non-space text. - Tabs are only for leading indentation, never for spaces after non-space text.
- TypeScript and TSX imports may stay on one line if they remain within the
line limit; do not expand short type-only imports mechanically.
- In TypeScript and TSX, when a function takes one destructured object
argument plus an inline type, prefer this shape when it fits locally:
```ts
const helper = (
{ value, flag }: { value: string
flag: boolean },
): Result => {
// ...
}
```
- In TypeScript and TSX, put `switch` case block braces on their own lines
when a case needs a lexical block:
```ts
case 'yes':
case 'no':
{
const expected = valueFor (item)
return expected == null || expected === answer
}
```
- In TypeScript and TSX, use `value == null` and `value != null` as the
default nullish checks. Do not use `=== null`, `=== undefined`,
`!== null`, or `!== undefined`.
- If code appears to need a distinction between `null` and `undefined`, treat
that as a design smell and revise the logic to avoid the distinction.
External library APIs that explicitly require distinguishing the two are the
only exception.
- In TypeScript and TSX, keep short arrays on one line when they fit under the
line limit; break arrays only when readability or line length requires it.
- In TypeScript and TSX, when a ternary expression is split across multiple
lines, align `?` and `:` with the condition expression. Do not indent `?` and
`:` one extra level under the condition.
```ts
const value =
condition
? consequent
: alternate
```
- In TypeScript and TSX, keep short ternary expressions on one line when they
fit cleanly under the line limit.
- Do not add production dependencies without explicit approval. - Do not add production dependencies without explicit approval.
- Do not create, modify, or run tests unless the user explicitly asks for - Do not create, modify, or run tests unless the user explicitly asks for
test work. When the user asks for tests, keep working and rerun them until test work. When the user asks for tests, keep working and rerun them until
+59 -83
ファイルの表示
@@ -1,43 +1,33 @@
import { expectedAnswerForQuestion } from '@/lib/gekanator' import { expectedAnswerForQuestion } from '@/lib/gekanator'
import type { import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion } from '@/lib/gekanator'
GekanatorAnswerLog,
GekanatorAnswerValue,
GekanatorQuestion,
} from '@/lib/gekanator'
import type { Post } from '@/types' import type { Post } from '@/types'
export type RecoveredCandidatePost = { export type RecoveredCandidatePost = {
postId: number postId: number
answerCountAtRecovery: number } answerCountAtRecovery: number }
const questionIsFactLikeForHardFiltering = ( const questionIsFactLikeForHardFiltering = (question: GekanatorQuestion): boolean =>
question: GekanatorQuestion,
): boolean =>
!(question.kind === 'post_similarity' !(question.kind === 'post_similarity'
|| ( || (question.kind === 'tag'
question.kind === 'tag'
&& question.condition.type === 'tag' && question.condition.type === 'tag'
&& !(question.condition.key.startsWith ('nico:')))) && !(question.condition.key.startsWith ('nico:'))))
export const candidatePostsFor = ({ export const candidatePostsFor = (
posts, { posts,
questions, questions,
answers, answers,
softenedQuestionIds, softenedQuestionIds,
rejectedPostIds, rejectedPostIds,
recoveredCandidatePosts, recoveredCandidatePosts }: { posts: Post[]
}: { questions: GekanatorQuestion[]
posts: Post[] answers: GekanatorAnswerLog[]
questions: GekanatorQuestion[] softenedQuestionIds: Set<string>
answers: GekanatorAnswerLog[] rejectedPostIds: Set<number>
softenedQuestionIds: Set<string> recoveredCandidatePosts: Map<number, number> },
rejectedPostIds: Set<number> ): Post[] => {
recoveredCandidatePosts: Map<number, number>
}): Post[] => {
const questionById = new Map (questions.map (question => [question.id, question])) const questionById = new Map (questions.map (question => [question.id, question]))
return posts.filter (post => { return posts.filter (post => {
@@ -47,7 +37,7 @@ export const candidatePostsFor = ({
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id) const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
return answers.every ((answer, index) => { return answers.every ((answer, index) => {
if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery) if (answerCountAtRecovery != null && index < answerCountAtRecovery)
return true return true
if (softenedQuestionIds.has (answer.questionId)) if (softenedQuestionIds.has (answer.questionId))
@@ -62,10 +52,11 @@ export const candidatePostsFor = ({
switch (answer.answer) switch (answer.answer)
{ {
case 'yes': case 'yes':
case 'no': { case 'no':
const expected = expectedAnswerForQuestion (question, post) {
return expected === null || expected === 'unknown' || expected === answer.answer const expected = expectedAnswerForQuestion (question, post)
} return expected === null || expected === 'unknown' || expected === answer.answer
}
default: default:
return true return true
} }
@@ -74,15 +65,11 @@ export const candidatePostsFor = ({
} }
export const hardFilteredPostsForAnswer = ({ export const hardFilteredPostsForAnswer = (
posts, { posts, question, answer }: { posts: Post[]
question, question: GekanatorQuestion
answer, answer: GekanatorAnswerValue },
}: { ): Post[] => {
posts: Post[]
question: GekanatorQuestion
answer: GekanatorAnswerValue
}): Post[] => {
if (!(questionIsFactLikeForHardFiltering (question))) if (!(questionIsFactLikeForHardFiltering (question)))
return posts return posts
@@ -91,16 +78,12 @@ export const hardFilteredPostsForAnswer = ({
return posts.filter (post => { return posts.filter (post => {
const expected = expectedAnswerForQuestion (question, post) const expected = expectedAnswerForQuestion (question, post)
return expected === null || expected === 'unknown' || expected === answer return expected == null || expected === 'unknown' || expected === answer
}) })
} }
const concreteAnswerOptions: GekanatorAnswerValue[] = [ const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', 'probably_no']
'yes',
'no',
'partial',
'probably_no']
export const allConcreteAnswerOptionsExhausted = ( export const allConcreteAnswerOptionsExhausted = (
@@ -119,45 +102,39 @@ const nextRecoveryTargetSize = (recoveryStepCount: number): number =>
6 * (2 ** recoveryStepCount) 6 * (2 ** recoveryStepCount)
export const recoverCandidatePosts = ({ export const recoverCandidatePosts = (
posts, { posts,
scores, scores,
rejectedPostIds, rejectedPostIds,
recoveredCandidatePosts, recoveredCandidatePosts,
eligiblePostIds, eligiblePostIds,
answerCountAtRecovery, answerCountAtRecovery,
recoveryStepCount, recoveryStepCount }: { posts: Post[]
}: { scores: Map<number, number>
posts: Post[] rejectedPostIds: Set<number>
scores: Map<number, number> recoveredCandidatePosts: Map<number, number>
rejectedPostIds: Set<number> eligiblePostIds: Set<number>
recoveredCandidatePosts: Map<number, number> answerCountAtRecovery: number
eligiblePostIds: Set<number> recoveryStepCount: number },
answerCountAtRecovery: number ): { recoveredCandidatePosts: Map<number, number>
recoveryStepCount: number recoveryStepCount: number } | null => {
}): {
recoveredCandidatePosts: Map<number, number>
recoveryStepCount: number
} | null => {
const recovered = new Map (recoveredCandidatePosts) const recovered = new Map (recoveredCandidatePosts)
const targetSize = nextRecoveryTargetSize (recoveryStepCount) const targetSize = nextRecoveryTargetSize (recoveryStepCount)
const countedPostIds = new Set ([ const countedPostIds = new Set ([...eligiblePostIds, ...recovered.keys ()])
...eligiblePostIds,
...recovered.keys ()])
const addCount = targetSize - countedPostIds.size const addCount = targetSize - countedPostIds.size
if (addCount <= 0) if (addCount <= 0)
return { {
recoveredCandidatePosts: recovered, return { recoveredCandidatePosts: recovered,
recoveryStepCount: recoveryStepCount + 1 } recoveryStepCount: recoveryStepCount + 1 }
}
const candidates = posts const candidates =
.filter (post => posts
!(rejectedPostIds.has (post.id)) .filter (post => (!(rejectedPostIds.has (post.id))
&& !(eligiblePostIds.has (post.id)) && !(eligiblePostIds.has (post.id))
&& !(recovered.has (post.id))) && !(recovered.has (post.id))))
.sort ((a, b) => .sort ((a, b) => ((scores.get (b.id) ?? Number.NEGATIVE_INFINITY)
(scores.get (b.id) ?? Number.NEGATIVE_INFINITY) - (scores.get (a.id) ?? Number.NEGATIVE_INFINITY)))
- (scores.get (a.id) ?? Number.NEGATIVE_INFINITY))
.slice (0, addCount) .slice (0, addCount)
if (candidates.length === 0) if (candidates.length === 0)
@@ -165,7 +142,6 @@ export const recoverCandidatePosts = ({
candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery)) candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery))
return { return { recoveredCandidatePosts: recovered,
recoveredCandidatePosts: recovered, recoveryStepCount: recoveryStepCount + 1 }
recoveryStepCount: recoveryStepCount + 1 }
} }
+93 -99
ファイルの表示
@@ -307,20 +307,19 @@ const shouldReplaceMergedQuestion = (
const hashString = (value: string): number => { const hashString = (value: string): number => {
let hash = 2166136261 let hash = 2_166_136_261
for (let i = 0; i < value.length; ++i) for (let i = 0; i < value.length; ++i)
{ {
hash ^= value.charCodeAt (i) hash ^= value.charCodeAt (i)
hash = Math.imul (hash, 16777619) hash = Math.imul (hash, 16_777_619)
} }
return hash >>> 0 return hash >>> 0
} }
const deterministicUnitFloat = (seed: string): number => const deterministicUnitFloat = (seed: string): number => hashString (seed) / 4_294_967_295
hashString (seed) / 4294967295
const clearStoredGame = (): void => { const clearStoredGame = (): void => {
@@ -366,16 +365,17 @@ const loadRecentGames = (): RecentGameSummary[] => {
if (!(Array.isArray (parsed))) if (!(Array.isArray (parsed)))
return [] return []
return parsed return (
.filter ((item): item is RecentGameSummary => parsed
.filter ((item): item is RecentGameSummary => (
typeof item === 'object' typeof item === 'object'
&& item !== null && item != null
&& Number.isInteger ((item as RecentGameSummary).correctPostId) && Number.isInteger ((item as RecentGameSummary).correctPostId)
&& (((item as RecentGameSummary).firstQuestionId === null) && (((item as RecentGameSummary).firstQuestionId == null)
|| typeof (item as RecentGameSummary).firstQuestionId === 'string') || typeof (item as RecentGameSummary).firstQuestionId === 'string')
&& Number.isFinite ((item as RecentGameSummary).savedAt)) && Number.isFinite ((item as RecentGameSummary).savedAt)))
.sort ((a, b) => b.savedAt - a.savedAt) .sort ((a, b) => b.savedAt - a.savedAt)
.slice (0, maxStoredRecentGames) .slice (0, maxStoredRecentGames))
} }
catch catch
{ {
@@ -387,13 +387,12 @@ const loadRecentGames = (): RecentGameSummary[] => {
const storeRecentGameSummary = ( const storeRecentGameSummary = (
summary: RecentGameSummary, summary: RecentGameSummary,
): RecentGameSummary[] => { ): RecentGameSummary[] => {
const next = [ const next =
summary, [summary,
...loadRecentGames ().filter (item => ...loadRecentGames ().filter (item => (item.savedAt !== summary.savedAt
item.savedAt !== summary.savedAt && !(item.correctPostId === summary.correctPostId
&& !( && (item.firstQuestionId
item.correctPostId === summary.correctPostId === summary.firstQuestionId))))]
&& item.firstQuestionId === summary.firstQuestionId))]
.slice (0, maxStoredRecentGames) .slice (0, maxStoredRecentGames)
try try
@@ -429,11 +428,10 @@ const loadBackgroundMotionMode = (): BackgroundMotionMode => {
const resettableExtraQuestionState = (): { const resettableExtraQuestionState = (): {
extraQuestions: GekanatorExtraQuestion[] extraQuestions: GekanatorExtraQuestion[]
extraQuestionAnswers: Record<string, GekanatorAnswerValue> extraQuestionAnswers: Record<string, GekanatorAnswerValue>
extraQuestionState: 'idle' extraQuestionState: 'idle' } => (
} => ({ { extraQuestions: [],
extraQuestions: [], extraQuestionAnswers: { },
extraQuestionAnswers: { }, extraQuestionState: 'idle' })
extraQuestionState: 'idle' })
const recoveredCandidateMapFromStored = ( const recoveredCandidateMapFromStored = (
@@ -445,9 +443,8 @@ const recoveredCandidateMapFromStored = (
const storedRecoveredCandidatesFromMap = ( const storedRecoveredCandidatesFromMap = (
recoveredCandidatePosts: Map<number, number>, recoveredCandidatePosts: Map<number, number>,
): RecoveredCandidatePost[] => ): RecoveredCandidatePost[] =>
[...recoveredCandidatePosts.entries ()].map (([postId, answerCountAtRecovery]) => ({ [...recoveredCandidatePosts.entries ()]
postId, .map (([postId, answerCountAtRecovery]) => ({ postId, answerCountAtRecovery }))
answerCountAtRecovery }))
const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => { const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => {
@@ -468,10 +465,7 @@ const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => {
const distributionEntropy = (weights: number[]): number => const distributionEntropy = (weights: number[]): number =>
weights.reduce ((sum, weight) => weights.reduce ((sum, weight) => weight <= 0 ? sum : sum - weight * Math.log2 (weight), 0)
weight <= 0
? sum
: sum - weight * Math.log2 (weight), 0)
const questionCategoryPenalty = ( const questionCategoryPenalty = (
@@ -481,9 +475,9 @@ const questionCategoryPenalty = (
): number => { ): number => {
const earlyFactor = Math.max (0, (3 - answerCount) / 3) const earlyFactor = Math.max (0, (3 - answerCount) / 3)
const titleLengthPenalty = const titleLengthPenalty =
titleLengthMinimumForCondition (question.condition) === null titleLengthMinimumForCondition (question.condition) == null
? 0 ? 0
: (answerCount === 0 ? 8 : 3.5) * earlyFactor : (answerCount === 0 ? 8 : 3.5) * earlyFactor
switch (question.kind) switch (question.kind)
{ {
@@ -673,11 +667,11 @@ const buildMaterialIndex = (
originalYearByPostId.set (post.id, originalYear) originalYearByPostId.set (post.id, originalYear)
originalMonthByPostId.set (post.id, originalMonth) originalMonthByPostId.set (post.id, originalMonth)
originalMonthDayByPostId.set (post.id, originalMonthDay) originalMonthDayByPostId.set (post.id, originalMonthDay)
if (originalYear !== null) if (originalYear != null)
addPostIdToIndex (postIdsByOriginalYear, originalYear, post.id) addPostIdToIndex (postIdsByOriginalYear, originalYear, post.id)
if (originalMonth !== null) if (originalMonth != null)
addPostIdToIndex (postIdsByOriginalMonth, originalMonth, post.id) addPostIdToIndex (postIdsByOriginalMonth, originalMonth, post.id)
if (originalMonthDay !== null) if (originalMonthDay != null)
addPostIdToIndex (postIdsByOriginalMonthDay, originalMonthDay, post.id) addPostIdToIndex (postIdsByOriginalMonthDay, originalMonthDay, post.id)
const titleLength = post.title?.length ?? 0 const titleLength = post.title?.length ?? 0
@@ -878,7 +872,7 @@ const matchingPostIdsForCondition = ({
case 'title-length-greater-than': { case 'title-length-greater-than': {
const threshold = const threshold =
titleLengthMinimumForCondition (condition) titleLengthMinimumForCondition (condition)
if (threshold === null) if (threshold == null)
return new Set<number> () return new Set<number> ()
const cached = materialIndex.titleLengthThresholdCache.get (threshold) const cached = materialIndex.titleLengthThresholdCache.get (threshold)
@@ -941,7 +935,7 @@ const matchingPostIdsForQuestion = ({
const byCondition = matchingPostIdsForCondition ({ const byCondition = matchingPostIdsForCondition ({
condition: question.condition, condition: question.condition,
materialIndex }) materialIndex })
if (byCondition !== null) if (byCondition != null)
return byCondition return byCondition
const matched = matchIndex.get (question.id) ?? dynamicMatchIndex?.get (question.id) const matched = matchIndex.get (question.id) ?? dynamicMatchIndex?.get (question.id)
@@ -1146,7 +1140,7 @@ const applyQuestionAnswerDeltaToScores = ({
const propagatedDelta = baseDelta * edge.cos const propagatedDelta = baseDelta * edge.cos
const current = propagatedDeltaByPostId.get (edge.targetPostId) const current = propagatedDeltaByPostId.get (edge.targetPostId)
if (current === undefined || Math.abs (propagatedDelta) > Math.abs (current)) if (current == null || Math.abs (propagatedDelta) > Math.abs (current))
propagatedDeltaByPostId.set (edge.targetPostId, propagatedDelta) propagatedDeltaByPostId.set (edge.targetPostId, propagatedDelta)
}) })
}) })
@@ -1204,13 +1198,13 @@ const buildQuestionsForCandidateIds = (
): GekanatorQuestion[] => { ): GekanatorQuestion[] => {
const total = candidateIds.length const total = candidateIds.length
const confirmationPost = const confirmationPost =
confirmationPostId === null confirmationPostId == null
? null ? null
: materialIndex.postById.get (confirmationPostId) ?? null : materialIndex.postById.get (confirmationPostId) ?? null
if (mode === 'split' && total === 0) if (mode === 'split' && total === 0)
return acceptedQuestions return acceptedQuestions
if (mode === 'confirmation' && confirmationPost === null) if (mode === 'confirmation' && confirmationPost == null)
return acceptedQuestions return acceptedQuestions
const tagCounts = new Map<string, number> () const tagCounts = new Map<string, number> ()
@@ -1228,7 +1222,7 @@ const buildQuestionsForCandidateIds = (
if (host) if (host)
hostCounts.set (host, (hostCounts.get (host) ?? 0) + 1) hostCounts.set (host, (hostCounts.get (host) ?? 0) + 1)
const year = materialIndex.originalYearByPostId.get (postId) const year = materialIndex.originalYearByPostId.get (postId)
if (year !== null && year !== undefined) if (year != null)
yearCounts.set (year, (yearCounts.get (year) ?? 0) + 1) yearCounts.set (year, (yearCounts.get (year) ?? 0) + 1)
const monthDay = materialIndex.originalMonthDayByPostId.get (postId) const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
if (monthDay) if (monthDay)
@@ -1272,7 +1266,7 @@ const buildQuestionsForCandidateIds = (
counts: monthDayCounts, counts: monthDayCounts,
total, total,
cap: factCap cap: factCap
}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) !== null) }).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null)
if (mode === 'split') if (mode === 'split')
{ {
@@ -1358,7 +1352,7 @@ const buildQuestionsForCandidateIds = (
materialIndex }) materialIndex })
: null) : null)
addQuestion ( addQuestion (
year !== null && year !== undefined year != null && year != undefined
? buildDateQuestion ({ ? buildDateQuestion ({
type: 'original-year', type: 'original-year',
year }) year })
@@ -1445,7 +1439,7 @@ const candidatePostsForState = ({
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id) const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
return answers.every ((answer, index) => { return answers.every ((answer, index) => {
if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery) if (answerCountAtRecovery != null && index < answerCountAtRecovery)
return true return true
if (softenedQuestionIds.has (answer.questionId)) if (softenedQuestionIds.has (answer.questionId))
return true return true
@@ -1468,17 +1462,17 @@ const candidatePostsForState = ({
condition, condition,
materialIndex }) materialIndex })
const useExpectedAnswer = const useExpectedAnswer =
question !== undefined question != null
&& usesLearnedTagExamples (question) && usesLearnedTagExamples (question)
if (question && !(questionIsFactLikeForHardFiltering (question))) if (question && !(questionIsFactLikeForHardFiltering (question)))
return true return true
if (matched !== null) if (matched != null)
{ {
if (useExpectedAnswer) if (useExpectedAnswer)
{ {
const expected = expectedAnswerForQuestion (question, post) const expected = expectedAnswerForQuestion (question, post)
return expected === null return expected == null
|| expected === 'unknown' || expected === 'unknown'
|| expected === answer.answer || expected === answer.answer
} }
@@ -1492,7 +1486,7 @@ const candidatePostsForState = ({
return true return true
const expected = expectedAnswerForQuestion (question, post) const expected = expectedAnswerForQuestion (question, post)
return expected === null || expected === 'unknown' || expected === answer.answer return expected == null || expected === 'unknown' || expected === answer.answer
}) })
}) })
} }
@@ -1648,7 +1642,7 @@ const previewAnswer = ({
dynamicMatchIndex }) dynamicMatchIndex })
const nextPosts = nextPostIds const nextPosts = nextPostIds
.map (postId => postById.get (postId)) .map (postId => postById.get (postId))
.filter ((post): post is Post => post !== undefined) .filter ((post): post is Post => post != null)
if (nextPosts.length === 0) if (nextPosts.length === 0)
return { return {
answer, answer,
@@ -1709,7 +1703,7 @@ const softenNextQuestionIds = ({
.filter ((item): item is { .filter ((item): item is {
answer: GekanatorAnswerLog answer: GekanatorAnswerLog
question: GekanatorQuestion } => question: GekanatorQuestion } =>
item.question !== undefined item.question != null
&& item.answer.answer !== 'unknown' && item.answer.answer !== 'unknown'
&& !(softenedQuestionIds.has (item.answer.questionId))) && !(softenedQuestionIds.has (item.answer.questionId)))
.sort ((a, b) => questionDifficulty (b.question) - questionDifficulty (a.question))[0] .sort ((a, b) => questionDifficulty (b.question) - questionDifficulty (a.question))[0]
@@ -1753,9 +1747,9 @@ const sameConditionValue = (
): boolean => { ): boolean => {
const leftTitleLength = titleLengthMinimumForCondition (left) const leftTitleLength = titleLengthMinimumForCondition (left)
const rightTitleLength = titleLengthMinimumForCondition (right) const rightTitleLength = titleLengthMinimumForCondition (right)
if (leftTitleLength !== null || rightTitleLength !== null) if (leftTitleLength != null || rightTitleLength != null)
return leftTitleLength !== null return leftTitleLength != null
&& rightTitleLength !== null && rightTitleLength != null
&& leftTitleLength === rightTitleLength && leftTitleLength === rightTitleLength
if (left.type !== right.type) if (left.type !== right.type)
@@ -1796,7 +1790,7 @@ const isMonthCrossMatch = (
): boolean => { ): boolean => {
const candidateMonth = monthForCondition (candidate) const candidateMonth = monthForCondition (candidate)
const previousMonth = monthForCondition (previous) const previousMonth = monthForCondition (previous)
if (candidateMonth === null || previousMonth === null) if (candidateMonth == null || previousMonth == null)
return false return false
const sameType = candidate.type === previous.type const sameType = candidate.type === previous.type
@@ -1814,12 +1808,12 @@ const isExclusiveContradiction = (
const candidateGroup = exclusiveConditionGroupFor (candidate) const candidateGroup = exclusiveConditionGroupFor (candidate)
const previousGroup = exclusiveConditionGroupFor (previous) const previousGroup = exclusiveConditionGroupFor (previous)
if (candidateGroup !== null && candidateGroup === previousGroup) if (candidateGroup != null && candidateGroup === previousGroup)
return !(sameConditionValue (candidate, previous)) return !(sameConditionValue (candidate, previous))
const candidateMonth = monthForCondition (candidate) const candidateMonth = monthForCondition (candidate)
const previousMonth = monthForCondition (previous) const previousMonth = monthForCondition (previous)
if (candidateMonth !== null && previousMonth !== null) if (candidateMonth != null && previousMonth != null)
return candidateMonth !== previousMonth return candidateMonth !== previousMonth
return false return false
@@ -2028,7 +2022,7 @@ const chooseQuestion = (
}, },
0) / priorWeightTotal)) 0) / priorWeightTotal))
const priorBonus = const priorBonus =
priorSplitScore === null priorSplitScore == null
? 0 ? 0
: Math.max (0, .22 - priorSplitScore) * -18 : Math.max (0, .22 - priorSplitScore) * -18
const infoGainBonus = -Math.min (1.2, infoGain) * 4 const infoGainBonus = -Math.min (1.2, infoGain) * 4
@@ -2050,7 +2044,7 @@ const chooseQuestion = (
.filter ((item): item is { .filter ((item): item is {
question: GekanatorQuestion question: GekanatorQuestion
score: number score: number
narrow: boolean } => item !== null && Number.isFinite (item.score)) narrow: boolean } => item != null && Number.isFinite (item.score))
.sort ((a, b) => a.score - b.score) .sort ((a, b) => a.score - b.score)
} }
@@ -2135,15 +2129,15 @@ const chooseWinningRunQuestion = ({
return false return false
const expected = expectedAnswerForQuestion (question, targetPost) const expected = expectedAnswerForQuestion (question, targetPost)
return expected !== null && expected !== 'unknown' return expected != null && expected !== 'unknown'
}) })
.map (question => { .map (question => {
const expected = expectedAnswerForQuestion (question, targetPost) const expected = expectedAnswerForQuestion (question, targetPost)
const priority = const priority =
expected === null expected == null
? null ? null
: winningRunPriorityFor (expected) : winningRunPriorityFor (expected)
if (priority === null) if (priority == null)
return null return null
const yesCount = matchingPostCountInIds ({ const yesCount = matchingPostCountInIds ({
@@ -2168,7 +2162,7 @@ const chooseWinningRunQuestion = ({
question: GekanatorQuestion question: GekanatorQuestion
priority: number priority: number
humanOffset: number humanOffset: number
matchingCount: number } => item !== null) matchingCount: number } => item != null)
.sort ((a, b) => { .sort ((a, b) => {
if (a.priority !== b.priority) if (a.priority !== b.priority)
return a.priority - b.priority return a.priority - b.priority
@@ -2258,7 +2252,7 @@ const chooseFallbackQuestion = ({
question: GekanatorQuestion question: GekanatorQuestion
knownCount: number knownCount: number
balance: number balance: number
humanOffset: number } => item !== null) humanOffset: number } => item != null)
.sort ((a, b) => { .sort ((a, b) => {
if (a.humanOffset !== b.humanOffset) if (a.humanOffset !== b.humanOffset)
return a.humanOffset - b.humanOffset return a.humanOffset - b.humanOffset
@@ -2291,13 +2285,13 @@ const isWinningRunActive = (
winningRunTargetId: number | null, winningRunTargetId: number | null,
winningRunStartAnswerCount: number | null, winningRunStartAnswerCount: number | null,
): boolean => ): boolean =>
winningRunTargetId !== null && winningRunStartAnswerCount !== null winningRunTargetId != null && winningRunStartAnswerCount != null
const winningRunQuestionCount = ( const winningRunQuestionCount = (
answers: GekanatorAnswerLog[], answers: GekanatorAnswerLog[],
winningRunStartAnswerCount: number | null, winningRunStartAnswerCount: number | null,
): number => winningRunStartAnswerCount === null ): number => winningRunStartAnswerCount == null
? 0 ? 0
: answers : answers
.slice (winningRunStartAnswerCount) .slice (winningRunStartAnswerCount)
@@ -2377,15 +2371,15 @@ const nextQuestionPlanFor = (
? eligiblePosts[0]?.id ?? null ? eligiblePosts[0]?.id ?? null
: null : null
const nextWinningRunStartAnswerCount = const nextWinningRunStartAnswerCount =
nextWinningRunTargetId === null nextWinningRunTargetId == null
? null ? null
: ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount) : ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
&& winningRunTargetId === nextWinningRunTargetId && winningRunTargetId === nextWinningRunTargetId
&& winningRunStartAnswerCount !== null) && winningRunStartAnswerCount != null)
? winningRunStartAnswerCount ? winningRunStartAnswerCount
: answers.length) : answers.length)
const nextWinningRunTargetPost = const nextWinningRunTargetPost =
nextWinningRunTargetId === null nextWinningRunTargetId == null
? null ? null
: posts.find (post => post.id === nextWinningRunTargetId) ?? null : posts.find (post => post.id === nextWinningRunTargetId) ?? null
const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] => const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] =>
@@ -2398,8 +2392,8 @@ const nextQuestionPlanFor = (
if (eligiblePosts.length === 1) if (eligiblePosts.length === 1)
{ {
const winningRunFinished = const winningRunFinished =
nextWinningRunTargetId !== null nextWinningRunTargetId != null
&& nextWinningRunStartAnswerCount !== null && nextWinningRunStartAnswerCount != null
&& eligiblePosts[0]?.id === nextWinningRunTargetId && eligiblePosts[0]?.id === nextWinningRunTargetId
&& winningRunQuestionCount ( && winningRunQuestionCount (
answers, answers,
@@ -2412,7 +2406,7 @@ const nextQuestionPlanFor = (
questionMode: null, questionMode: null,
winningRunTargetId: nextWinningRunTargetId, winningRunTargetId: nextWinningRunTargetId,
winningRunStartAnswerCount: nextWinningRunStartAnswerCount } winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount === null) if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount == null)
return { return {
question: null, question: null,
guess: null, guess: null,
@@ -2586,9 +2580,9 @@ const backgroundPostsFor = ({
}): Post[] => { }): Post[] => {
const focusPosts = const focusPosts =
phase === 'end' || phase === 'review' || phase === 'learned' phase === 'end' || phase === 'review' || phase === 'learned'
? [reviewCorrectPost, reviewGuessedPost].filter ((post): post is Post => post !== null) ? [reviewCorrectPost, reviewGuessedPost].filter ((post): post is Post => post != null)
: phase === 'guess' : phase === 'guess'
? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post !== null) ? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post != null)
: eligiblePosts.length > 0 : eligiblePosts.length > 0
? eligiblePosts ? eligiblePosts
: availablePosts : availablePosts
@@ -2646,7 +2640,7 @@ const GekanatorBackdrop: FC<{
const isWinningRunBackdrop = const isWinningRunBackdrop =
!(guessThumbnail) !(guessThumbnail)
&& phase === 'question' && phase === 'question'
&& winningRunTargetPost !== null && winningRunTargetPost != null
&& Boolean (backgroundThumbnailUrl (winningRunTargetPost)) && Boolean (backgroundThumbnailUrl (winningRunTargetPost))
const backdropMode = const backdropMode =
guessThumbnail guessThumbnail
@@ -2843,7 +2837,7 @@ const GekanatorBackdrop: FC<{
setActiveDirection (nextDirection) setActiveDirection (nextDirection)
} }
if (flipTimerRef.current !== null) if (flipTimerRef.current != null)
{ {
window.clearTimeout (flipTimerRef.current) window.clearTimeout (flipTimerRef.current)
flipTimerRef.current = null flipTimerRef.current = null
@@ -2924,7 +2918,7 @@ const GekanatorBackdrop: FC<{
}, tileFlipDuration * 1000) }, tileFlipDuration * 1000)
return () => { return () => {
if (flipTimerRef.current !== null) if (flipTimerRef.current != null)
{ {
window.clearTimeout (flipTimerRef.current) window.clearTimeout (flipTimerRef.current)
flipTimerRef.current = null flipTimerRef.current = null
@@ -3072,10 +3066,10 @@ const expectedAnswerFor = (
const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const storedGame = useMemo (loadStoredGame, []) const storedGame = useMemo (loadStoredGame, [])
const hasStoredRestore = storedGame !== null && isStoredPhase (storedGame.phase) const hasStoredRestore = storedGame != null && isStoredPhase (storedGame.phase)
const queryClient = useQueryClient () const queryClient = useQueryClient ()
const isAdmin = user?.role === 'admin' const isAdmin = user?.role === 'admin'
const canPersistGame = user !== null const canPersistGame = user != null
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> ( const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
() => loadRecentGames ()) () => loadRecentGames ())
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> ( const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
@@ -3188,7 +3182,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setAskedQuestionBank ( setAskedQuestionBank (
storedAskedQuestionBankIds storedAskedQuestionBankIds
.map (questionId => questionById.get (questionId)) .map (questionId => questionById.get (questionId))
.filter ((question): question is GekanatorQuestion => question !== undefined)) .filter ((question): question is GekanatorQuestion => question != null))
setStoredAskedQuestionBankIds ([]) setStoredAskedQuestionBankIds ([])
}, [posts, }, [posts,
storedAskedQuestionBankIds, storedAskedQuestionBankIds,
@@ -3340,11 +3334,11 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
?? null, ?? null,
[acceptedQuestions, questionSuggestionSelectedId]) [acceptedQuestions, questionSuggestionSelectedId])
const canSubmitQuestionSuggestion = useMemo (() => { const canSubmitQuestionSuggestion = useMemo (() => {
if (!(canPersistGame) || reviewCorrectPostId === null) if (!(canPersistGame) || reviewCorrectPostId == null)
return false return false
if (questionSuggestionEntryMode === 'search') if (questionSuggestionEntryMode === 'search')
return selectedSuggestedQuestion !== null return selectedSuggestedQuestion != null
return questionSuggestion.trim () !== '' return questionSuggestion.trim () !== ''
}, [ }, [
@@ -3403,7 +3397,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
userPriorWeights, materialIndex, acceptedQuestionMatchIndex, userPriorWeights, materialIndex, acceptedQuestionMatchIndex,
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount]) lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
const winningRunTargetPost = useMemo ( const winningRunTargetPost = useMemo (
() => questionPlan.winningRunTargetId === null () => questionPlan.winningRunTargetId == null
? null ? null
: posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null, : posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null,
[posts, questionPlan.winningRunTargetId]) [posts, questionPlan.winningRunTargetId])
@@ -3417,7 +3411,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
&& winningRunQuestionsAsked < winningRunQuestionLimit && winningRunQuestionsAsked < winningRunQuestionLimit
&& eligiblePosts.length === 1 && eligiblePosts.length === 1
&& eligiblePosts[0]?.id === questionPlan.winningRunTargetId && eligiblePosts[0]?.id === questionPlan.winningRunTargetId
&& winningRunTargetPost !== null && winningRunTargetPost != null
const topScoredPosts = useMemo ( const topScoredPosts = useMemo (
() => eligiblePosts () => eligiblePosts
.map (post => ({ post, score: scores.get (post.id) ?? 0 })) .map (post => ({ post, score: scores.get (post.id) ?? 0 }))
@@ -3454,7 +3448,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
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 && reviewCorrectPostId != null)
? reviewGuessedPostId === reviewCorrectPostId ? reviewGuessedPostId === reviewCorrectPostId
: null) : null)
const effectiveBackgroundMotionMode = const effectiveBackgroundMotionMode =
@@ -3895,7 +3889,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
} }
const startReview = () => { const startReview = () => {
if (reviewGuessedPostId === null || reviewCorrectPostId === null) if (reviewGuessedPostId == null || reviewCorrectPostId == null)
return return
saveMutation.reset () saveMutation.reset ()
@@ -3915,13 +3909,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const saveReviewedResult = (onSuccess: (gameId: number) => void) => { const saveReviewedResult = (onSuccess: (gameId: number) => void) => {
if ( if (
!(canPersistGame) !(canPersistGame)
|| reviewGuessedPostId === null || reviewGuessedPostId == null
|| reviewCorrectPostId === null || reviewCorrectPostId == null
|| saveMutation.isPending || saveMutation.isPending
) )
return return
if (savedGameId !== null) if (savedGameId != null)
{ {
onSuccess (savedGameId) onSuccess (savedGameId)
return return
@@ -3979,7 +3973,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const saveExtraQuestions = () => { const saveExtraQuestions = () => {
if ( if (
!(canPersistGame) !(canPersistGame)
|| savedGameId === null || savedGameId == null
|| extraQuestionAnswersMutation.isPending || extraQuestionAnswersMutation.isPending
|| extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)])) || extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)]))
) )
@@ -4175,7 +4169,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
} }
const startExtraQuestions = () => { const startExtraQuestions = () => {
if (reviewCorrectPostId === null || saveMutation.isPending) if (reviewCorrectPostId == null || saveMutation.isPending)
return return
saveReviewedResult (gameId => { saveReviewedResult (gameId => {
@@ -4206,7 +4200,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const dialogue = phase === 'learned' ? resultDialogue : introDialogue const dialogue = phase === 'learned' ? resultDialogue : introDialogue
const saveStatusMessage = const saveStatusMessage =
saved saved
&& learnedExampleCount !== null && learnedExampleCount != null
? learnedExampleCount > 0 ? learnedExampleCount > 0
? `${ learnedExampleCount }件の回答を学習しました` ? `${ learnedExampleCount }件の回答を学習しました`
: null : null
@@ -4436,7 +4430,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{' / '} {' / '}
recoveryStepCount: {recoveryStepCount} recoveryStepCount: {recoveryStepCount}
{' / '} {' / '}
currentQuestion===null: {String (currentQuestion === null)} currentQuestion===null: {String (currentQuestion == null)}
</div> </div>
{topScoredPosts.length > 0 && ( {topScoredPosts.length > 0 && (
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1"> <div className="mt-1 flex flex-wrap gap-x-4 gap-y-1">
@@ -4525,7 +4519,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{' / '} {' / '}
recoveryStepCount: {recoveryStepCount} recoveryStepCount: {recoveryStepCount}
{' / '} {' / '}
currentQuestion===null: {String (currentQuestion === null)} currentQuestion===null: {String (currentQuestion == null)}
</div>)} </div>)}
<PostMiniCard post={displayedGuess}/> <PostMiniCard post={displayedGuess}/>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@@ -4621,7 +4615,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</button> </button>
</div> </div>
{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>)}
@@ -4648,7 +4642,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
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
hover:bg-pink-500 disabled:opacity-50" hover:bg-pink-500 disabled:opacity-50"
disabled={reviewCorrectPostId === null || saveMutation.isPending} disabled={reviewCorrectPostId == null || saveMutation.isPending}
onClick={saveAndReset}> onClick={saveAndReset}>
</button> </button>
@@ -4659,7 +4653,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
dark:hover:bg-red-900 disabled:opacity-50" dark:hover:bg-red-900 disabled:opacity-50"
disabled={ disabled={
!(canPersistGame) !(canPersistGame)
|| reviewCorrectPostId === null || reviewCorrectPostId == null
|| saveMutation.isPending || saveMutation.isPending
} }
onClick={startReview}> onClick={startReview}>
@@ -4682,7 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
hover:bg-yellow-100 dark:border-red-700 hover:bg-yellow-100 dark:border-red-700
dark:hover:bg-red-900 disabled:opacity-50" dark:hover:bg-red-900 disabled:opacity-50"
disabled={!(canPersistGame) disabled={!(canPersistGame)
|| reviewCorrectPostId === null || reviewCorrectPostId == null
|| saveMutation.isPending || saveMutation.isPending
|| extraQuestionState === 'loading' || extraQuestionState === 'loading'
|| extraQuestionAnswersMutation.isPending} || extraQuestionAnswersMutation.isPending}
@@ -4768,7 +4762,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</div> </div>
</div> </div>
{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 ? '当たり' : 'はずれ'}
@@ -4798,7 +4792,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
hover:bg-pink-500 disabled:opacity-50" hover:bg-pink-500 disabled:opacity-50"
disabled={ disabled={
!(canPersistGame) !(canPersistGame)
|| reviewCorrectPostId === null || reviewCorrectPostId == null
|| saveMutation.isPending || saveMutation.isPending
|| questionSuggestionMutation.isPending || questionSuggestionMutation.isPending
} }