このコミットが含まれているのは:
@@ -1,43 +1,33 @@
|
||||
import { expectedAnswerForQuestion } from '@/lib/gekanator'
|
||||
|
||||
import type {
|
||||
GekanatorAnswerLog,
|
||||
GekanatorAnswerValue,
|
||||
GekanatorQuestion,
|
||||
} from '@/lib/gekanator'
|
||||
import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion } from '@/lib/gekanator'
|
||||
import type { Post } from '@/types'
|
||||
|
||||
|
||||
export type RecoveredCandidatePost = {
|
||||
postId: number
|
||||
answerCountAtRecovery: number }
|
||||
|
||||
|
||||
const questionIsFactLikeForHardFiltering = (
|
||||
question: GekanatorQuestion,
|
||||
): boolean =>
|
||||
const questionIsFactLikeForHardFiltering = (question: GekanatorQuestion): boolean =>
|
||||
!(question.kind === 'post_similarity'
|
||||
|| (
|
||||
question.kind === 'tag'
|
||||
|| (question.kind === 'tag'
|
||||
&& question.condition.type === 'tag'
|
||||
&& !(question.condition.key.startsWith ('nico:'))))
|
||||
|
||||
|
||||
export const candidatePostsFor = ({
|
||||
posts,
|
||||
questions,
|
||||
answers,
|
||||
softenedQuestionIds,
|
||||
rejectedPostIds,
|
||||
recoveredCandidatePosts,
|
||||
}: {
|
||||
posts: Post[]
|
||||
questions: GekanatorQuestion[]
|
||||
answers: GekanatorAnswerLog[]
|
||||
softenedQuestionIds: Set<string>
|
||||
rejectedPostIds: Set<number>
|
||||
recoveredCandidatePosts: Map<number, number>
|
||||
}): Post[] => {
|
||||
export const candidatePostsFor = (
|
||||
{ posts,
|
||||
questions,
|
||||
answers,
|
||||
softenedQuestionIds,
|
||||
rejectedPostIds,
|
||||
recoveredCandidatePosts }: { posts: Post[]
|
||||
questions: GekanatorQuestion[]
|
||||
answers: GekanatorAnswerLog[]
|
||||
softenedQuestionIds: Set<string>
|
||||
rejectedPostIds: Set<number>
|
||||
recoveredCandidatePosts: Map<number, number> },
|
||||
): Post[] => {
|
||||
const questionById = new Map (questions.map (question => [question.id, question]))
|
||||
|
||||
return posts.filter (post => {
|
||||
@@ -47,7 +37,7 @@ export const candidatePostsFor = ({
|
||||
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
|
||||
|
||||
return answers.every ((answer, index) => {
|
||||
if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery)
|
||||
if (answerCountAtRecovery != null && index < answerCountAtRecovery)
|
||||
return true
|
||||
|
||||
if (softenedQuestionIds.has (answer.questionId))
|
||||
@@ -62,10 +52,11 @@ export const candidatePostsFor = ({
|
||||
switch (answer.answer)
|
||||
{
|
||||
case 'yes':
|
||||
case 'no': {
|
||||
const expected = expectedAnswerForQuestion (question, post)
|
||||
return expected === null || expected === 'unknown' || expected === answer.answer
|
||||
}
|
||||
case 'no':
|
||||
{
|
||||
const expected = expectedAnswerForQuestion (question, post)
|
||||
return expected === null || expected === 'unknown' || expected === answer.answer
|
||||
}
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@@ -74,15 +65,11 @@ export const candidatePostsFor = ({
|
||||
}
|
||||
|
||||
|
||||
export const hardFilteredPostsForAnswer = ({
|
||||
posts,
|
||||
question,
|
||||
answer,
|
||||
}: {
|
||||
posts: Post[]
|
||||
question: GekanatorQuestion
|
||||
answer: GekanatorAnswerValue
|
||||
}): Post[] => {
|
||||
export const hardFilteredPostsForAnswer = (
|
||||
{ posts, question, answer }: { posts: Post[]
|
||||
question: GekanatorQuestion
|
||||
answer: GekanatorAnswerValue },
|
||||
): Post[] => {
|
||||
if (!(questionIsFactLikeForHardFiltering (question)))
|
||||
return posts
|
||||
|
||||
@@ -91,16 +78,12 @@ export const hardFilteredPostsForAnswer = ({
|
||||
|
||||
return posts.filter (post => {
|
||||
const expected = expectedAnswerForQuestion (question, post)
|
||||
return expected === null || expected === 'unknown' || expected === answer
|
||||
return expected == null || expected === 'unknown' || expected === answer
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const concreteAnswerOptions: GekanatorAnswerValue[] = [
|
||||
'yes',
|
||||
'no',
|
||||
'partial',
|
||||
'probably_no']
|
||||
const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', 'probably_no']
|
||||
|
||||
|
||||
export const allConcreteAnswerOptionsExhausted = (
|
||||
@@ -119,45 +102,39 @@ const nextRecoveryTargetSize = (recoveryStepCount: number): number =>
|
||||
6 * (2 ** recoveryStepCount)
|
||||
|
||||
|
||||
export const recoverCandidatePosts = ({
|
||||
posts,
|
||||
scores,
|
||||
rejectedPostIds,
|
||||
recoveredCandidatePosts,
|
||||
eligiblePostIds,
|
||||
answerCountAtRecovery,
|
||||
recoveryStepCount,
|
||||
}: {
|
||||
posts: Post[]
|
||||
scores: Map<number, number>
|
||||
rejectedPostIds: Set<number>
|
||||
recoveredCandidatePosts: Map<number, number>
|
||||
eligiblePostIds: Set<number>
|
||||
answerCountAtRecovery: number
|
||||
recoveryStepCount: number
|
||||
}): {
|
||||
recoveredCandidatePosts: Map<number, number>
|
||||
recoveryStepCount: number
|
||||
} | null => {
|
||||
export const recoverCandidatePosts = (
|
||||
{ posts,
|
||||
scores,
|
||||
rejectedPostIds,
|
||||
recoveredCandidatePosts,
|
||||
eligiblePostIds,
|
||||
answerCountAtRecovery,
|
||||
recoveryStepCount }: { posts: Post[]
|
||||
scores: Map<number, number>
|
||||
rejectedPostIds: Set<number>
|
||||
recoveredCandidatePosts: Map<number, number>
|
||||
eligiblePostIds: Set<number>
|
||||
answerCountAtRecovery: number
|
||||
recoveryStepCount: number },
|
||||
): { recoveredCandidatePosts: Map<number, number>
|
||||
recoveryStepCount: number } | null => {
|
||||
const recovered = new Map (recoveredCandidatePosts)
|
||||
const targetSize = nextRecoveryTargetSize (recoveryStepCount)
|
||||
const countedPostIds = new Set ([
|
||||
...eligiblePostIds,
|
||||
...recovered.keys ()])
|
||||
const countedPostIds = new Set ([...eligiblePostIds, ...recovered.keys ()])
|
||||
const addCount = targetSize - countedPostIds.size
|
||||
if (addCount <= 0)
|
||||
return {
|
||||
recoveredCandidatePosts: recovered,
|
||||
recoveryStepCount: recoveryStepCount + 1 }
|
||||
{
|
||||
return { recoveredCandidatePosts: recovered,
|
||||
recoveryStepCount: recoveryStepCount + 1 }
|
||||
}
|
||||
|
||||
const candidates = posts
|
||||
.filter (post =>
|
||||
!(rejectedPostIds.has (post.id))
|
||||
&& !(eligiblePostIds.has (post.id))
|
||||
&& !(recovered.has (post.id)))
|
||||
.sort ((a, b) =>
|
||||
(scores.get (b.id) ?? Number.NEGATIVE_INFINITY)
|
||||
- (scores.get (a.id) ?? Number.NEGATIVE_INFINITY))
|
||||
const candidates =
|
||||
posts
|
||||
.filter (post => (!(rejectedPostIds.has (post.id))
|
||||
&& !(eligiblePostIds.has (post.id))
|
||||
&& !(recovered.has (post.id))))
|
||||
.sort ((a, b) => ((scores.get (b.id) ?? Number.NEGATIVE_INFINITY)
|
||||
- (scores.get (a.id) ?? Number.NEGATIVE_INFINITY)))
|
||||
.slice (0, addCount)
|
||||
|
||||
if (candidates.length === 0)
|
||||
@@ -165,7 +142,6 @@ export const recoverCandidatePosts = ({
|
||||
|
||||
candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery))
|
||||
|
||||
return {
|
||||
recoveredCandidatePosts: recovered,
|
||||
recoveryStepCount: recoveryStepCount + 1 }
|
||||
return { recoveredCandidatePosts: recovered,
|
||||
recoveryStepCount: recoveryStepCount + 1 }
|
||||
}
|
||||
|
||||
@@ -307,20 +307,19 @@ const shouldReplaceMergedQuestion = (
|
||||
|
||||
|
||||
const hashString = (value: string): number => {
|
||||
let hash = 2166136261
|
||||
let hash = 2_166_136_261
|
||||
|
||||
for (let i = 0; i < value.length; ++i)
|
||||
{
|
||||
hash ^= value.charCodeAt (i)
|
||||
hash = Math.imul (hash, 16777619)
|
||||
hash = Math.imul (hash, 16_777_619)
|
||||
}
|
||||
|
||||
return hash >>> 0
|
||||
}
|
||||
|
||||
|
||||
const deterministicUnitFloat = (seed: string): number =>
|
||||
hashString (seed) / 4294967295
|
||||
const deterministicUnitFloat = (seed: string): number => hashString (seed) / 4_294_967_295
|
||||
|
||||
|
||||
const clearStoredGame = (): void => {
|
||||
@@ -366,16 +365,17 @@ const loadRecentGames = (): RecentGameSummary[] => {
|
||||
if (!(Array.isArray (parsed)))
|
||||
return []
|
||||
|
||||
return parsed
|
||||
.filter ((item): item is RecentGameSummary =>
|
||||
return (
|
||||
parsed
|
||||
.filter ((item): item is RecentGameSummary => (
|
||||
typeof item === 'object'
|
||||
&& item !== null
|
||||
&& item != null
|
||||
&& Number.isInteger ((item as RecentGameSummary).correctPostId)
|
||||
&& (((item as RecentGameSummary).firstQuestionId === null)
|
||||
&& (((item as RecentGameSummary).firstQuestionId == null)
|
||||
|| 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)
|
||||
.slice (0, maxStoredRecentGames)
|
||||
.slice (0, maxStoredRecentGames))
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -387,13 +387,12 @@ const loadRecentGames = (): RecentGameSummary[] => {
|
||||
const storeRecentGameSummary = (
|
||||
summary: RecentGameSummary,
|
||||
): RecentGameSummary[] => {
|
||||
const next = [
|
||||
summary,
|
||||
...loadRecentGames ().filter (item =>
|
||||
item.savedAt !== summary.savedAt
|
||||
&& !(
|
||||
item.correctPostId === summary.correctPostId
|
||||
&& item.firstQuestionId === summary.firstQuestionId))]
|
||||
const next =
|
||||
[summary,
|
||||
...loadRecentGames ().filter (item => (item.savedAt !== summary.savedAt
|
||||
&& !(item.correctPostId === summary.correctPostId
|
||||
&& (item.firstQuestionId
|
||||
=== summary.firstQuestionId))))]
|
||||
.slice (0, maxStoredRecentGames)
|
||||
|
||||
try
|
||||
@@ -429,11 +428,10 @@ const loadBackgroundMotionMode = (): BackgroundMotionMode => {
|
||||
const resettableExtraQuestionState = (): {
|
||||
extraQuestions: GekanatorExtraQuestion[]
|
||||
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
|
||||
extraQuestionState: 'idle'
|
||||
} => ({
|
||||
extraQuestions: [],
|
||||
extraQuestionAnswers: { },
|
||||
extraQuestionState: 'idle' })
|
||||
extraQuestionState: 'idle' } => (
|
||||
{ extraQuestions: [],
|
||||
extraQuestionAnswers: { },
|
||||
extraQuestionState: 'idle' })
|
||||
|
||||
|
||||
const recoveredCandidateMapFromStored = (
|
||||
@@ -445,9 +443,8 @@ const recoveredCandidateMapFromStored = (
|
||||
const storedRecoveredCandidatesFromMap = (
|
||||
recoveredCandidatePosts: Map<number, number>,
|
||||
): RecoveredCandidatePost[] =>
|
||||
[...recoveredCandidatePosts.entries ()].map (([postId, answerCountAtRecovery]) => ({
|
||||
postId,
|
||||
answerCountAtRecovery }))
|
||||
[...recoveredCandidatePosts.entries ()]
|
||||
.map (([postId, answerCountAtRecovery]) => ({ postId, answerCountAtRecovery }))
|
||||
|
||||
|
||||
const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => {
|
||||
@@ -468,10 +465,7 @@ const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => {
|
||||
|
||||
|
||||
const distributionEntropy = (weights: number[]): number =>
|
||||
weights.reduce ((sum, weight) =>
|
||||
weight <= 0
|
||||
? sum
|
||||
: sum - weight * Math.log2 (weight), 0)
|
||||
weights.reduce ((sum, weight) => weight <= 0 ? sum : sum - weight * Math.log2 (weight), 0)
|
||||
|
||||
|
||||
const questionCategoryPenalty = (
|
||||
@@ -481,9 +475,9 @@ const questionCategoryPenalty = (
|
||||
): number => {
|
||||
const earlyFactor = Math.max (0, (3 - answerCount) / 3)
|
||||
const titleLengthPenalty =
|
||||
titleLengthMinimumForCondition (question.condition) === null
|
||||
? 0
|
||||
: (answerCount === 0 ? 8 : 3.5) * earlyFactor
|
||||
titleLengthMinimumForCondition (question.condition) == null
|
||||
? 0
|
||||
: (answerCount === 0 ? 8 : 3.5) * earlyFactor
|
||||
|
||||
switch (question.kind)
|
||||
{
|
||||
@@ -673,11 +667,11 @@ const buildMaterialIndex = (
|
||||
originalYearByPostId.set (post.id, originalYear)
|
||||
originalMonthByPostId.set (post.id, originalMonth)
|
||||
originalMonthDayByPostId.set (post.id, originalMonthDay)
|
||||
if (originalYear !== null)
|
||||
if (originalYear != null)
|
||||
addPostIdToIndex (postIdsByOriginalYear, originalYear, post.id)
|
||||
if (originalMonth !== null)
|
||||
if (originalMonth != null)
|
||||
addPostIdToIndex (postIdsByOriginalMonth, originalMonth, post.id)
|
||||
if (originalMonthDay !== null)
|
||||
if (originalMonthDay != null)
|
||||
addPostIdToIndex (postIdsByOriginalMonthDay, originalMonthDay, post.id)
|
||||
|
||||
const titleLength = post.title?.length ?? 0
|
||||
@@ -878,7 +872,7 @@ const matchingPostIdsForCondition = ({
|
||||
case 'title-length-greater-than': {
|
||||
const threshold =
|
||||
titleLengthMinimumForCondition (condition)
|
||||
if (threshold === null)
|
||||
if (threshold == null)
|
||||
return new Set<number> ()
|
||||
|
||||
const cached = materialIndex.titleLengthThresholdCache.get (threshold)
|
||||
@@ -941,7 +935,7 @@ const matchingPostIdsForQuestion = ({
|
||||
const byCondition = matchingPostIdsForCondition ({
|
||||
condition: question.condition,
|
||||
materialIndex })
|
||||
if (byCondition !== null)
|
||||
if (byCondition != null)
|
||||
return byCondition
|
||||
|
||||
const matched = matchIndex.get (question.id) ?? dynamicMatchIndex?.get (question.id)
|
||||
@@ -1146,7 +1140,7 @@ const applyQuestionAnswerDeltaToScores = ({
|
||||
|
||||
const propagatedDelta = baseDelta * edge.cos
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -1204,13 +1198,13 @@ const buildQuestionsForCandidateIds = (
|
||||
): GekanatorQuestion[] => {
|
||||
const total = candidateIds.length
|
||||
const confirmationPost =
|
||||
confirmationPostId === null
|
||||
confirmationPostId == null
|
||||
? null
|
||||
: materialIndex.postById.get (confirmationPostId) ?? null
|
||||
|
||||
if (mode === 'split' && total === 0)
|
||||
return acceptedQuestions
|
||||
if (mode === 'confirmation' && confirmationPost === null)
|
||||
if (mode === 'confirmation' && confirmationPost == null)
|
||||
return acceptedQuestions
|
||||
|
||||
const tagCounts = new Map<string, number> ()
|
||||
@@ -1228,7 +1222,7 @@ const buildQuestionsForCandidateIds = (
|
||||
if (host)
|
||||
hostCounts.set (host, (hostCounts.get (host) ?? 0) + 1)
|
||||
const year = materialIndex.originalYearByPostId.get (postId)
|
||||
if (year !== null && year !== undefined)
|
||||
if (year != null)
|
||||
yearCounts.set (year, (yearCounts.get (year) ?? 0) + 1)
|
||||
const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
|
||||
if (monthDay)
|
||||
@@ -1272,7 +1266,7 @@ const buildQuestionsForCandidateIds = (
|
||||
counts: monthDayCounts,
|
||||
total,
|
||||
cap: factCap
|
||||
}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) !== null)
|
||||
}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null)
|
||||
|
||||
if (mode === 'split')
|
||||
{
|
||||
@@ -1358,7 +1352,7 @@ const buildQuestionsForCandidateIds = (
|
||||
materialIndex })
|
||||
: null)
|
||||
addQuestion (
|
||||
year !== null && year !== undefined
|
||||
year != null && year != undefined
|
||||
? buildDateQuestion ({
|
||||
type: 'original-year',
|
||||
year })
|
||||
@@ -1445,7 +1439,7 @@ const candidatePostsForState = ({
|
||||
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
|
||||
|
||||
return answers.every ((answer, index) => {
|
||||
if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery)
|
||||
if (answerCountAtRecovery != null && index < answerCountAtRecovery)
|
||||
return true
|
||||
if (softenedQuestionIds.has (answer.questionId))
|
||||
return true
|
||||
@@ -1468,17 +1462,17 @@ const candidatePostsForState = ({
|
||||
condition,
|
||||
materialIndex })
|
||||
const useExpectedAnswer =
|
||||
question !== undefined
|
||||
question != null
|
||||
&& usesLearnedTagExamples (question)
|
||||
if (question && !(questionIsFactLikeForHardFiltering (question)))
|
||||
return true
|
||||
|
||||
if (matched !== null)
|
||||
if (matched != null)
|
||||
{
|
||||
if (useExpectedAnswer)
|
||||
{
|
||||
const expected = expectedAnswerForQuestion (question, post)
|
||||
return expected === null
|
||||
return expected == null
|
||||
|| expected === 'unknown'
|
||||
|| expected === answer.answer
|
||||
}
|
||||
@@ -1492,7 +1486,7 @@ const candidatePostsForState = ({
|
||||
return true
|
||||
|
||||
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 })
|
||||
const nextPosts = nextPostIds
|
||||
.map (postId => postById.get (postId))
|
||||
.filter ((post): post is Post => post !== undefined)
|
||||
.filter ((post): post is Post => post != null)
|
||||
if (nextPosts.length === 0)
|
||||
return {
|
||||
answer,
|
||||
@@ -1709,7 +1703,7 @@ const softenNextQuestionIds = ({
|
||||
.filter ((item): item is {
|
||||
answer: GekanatorAnswerLog
|
||||
question: GekanatorQuestion } =>
|
||||
item.question !== undefined
|
||||
item.question != null
|
||||
&& item.answer.answer !== 'unknown'
|
||||
&& !(softenedQuestionIds.has (item.answer.questionId)))
|
||||
.sort ((a, b) => questionDifficulty (b.question) - questionDifficulty (a.question))[0]
|
||||
@@ -1753,9 +1747,9 @@ const sameConditionValue = (
|
||||
): boolean => {
|
||||
const leftTitleLength = titleLengthMinimumForCondition (left)
|
||||
const rightTitleLength = titleLengthMinimumForCondition (right)
|
||||
if (leftTitleLength !== null || rightTitleLength !== null)
|
||||
return leftTitleLength !== null
|
||||
&& rightTitleLength !== null
|
||||
if (leftTitleLength != null || rightTitleLength != null)
|
||||
return leftTitleLength != null
|
||||
&& rightTitleLength != null
|
||||
&& leftTitleLength === rightTitleLength
|
||||
|
||||
if (left.type !== right.type)
|
||||
@@ -1796,7 +1790,7 @@ const isMonthCrossMatch = (
|
||||
): boolean => {
|
||||
const candidateMonth = monthForCondition (candidate)
|
||||
const previousMonth = monthForCondition (previous)
|
||||
if (candidateMonth === null || previousMonth === null)
|
||||
if (candidateMonth == null || previousMonth == null)
|
||||
return false
|
||||
|
||||
const sameType = candidate.type === previous.type
|
||||
@@ -1814,12 +1808,12 @@ const isExclusiveContradiction = (
|
||||
const candidateGroup = exclusiveConditionGroupFor (candidate)
|
||||
const previousGroup = exclusiveConditionGroupFor (previous)
|
||||
|
||||
if (candidateGroup !== null && candidateGroup === previousGroup)
|
||||
if (candidateGroup != null && candidateGroup === previousGroup)
|
||||
return !(sameConditionValue (candidate, previous))
|
||||
|
||||
const candidateMonth = monthForCondition (candidate)
|
||||
const previousMonth = monthForCondition (previous)
|
||||
if (candidateMonth !== null && previousMonth !== null)
|
||||
if (candidateMonth != null && previousMonth != null)
|
||||
return candidateMonth !== previousMonth
|
||||
|
||||
return false
|
||||
@@ -2028,7 +2022,7 @@ const chooseQuestion = (
|
||||
},
|
||||
0) / priorWeightTotal))
|
||||
const priorBonus =
|
||||
priorSplitScore === null
|
||||
priorSplitScore == null
|
||||
? 0
|
||||
: Math.max (0, .22 - priorSplitScore) * -18
|
||||
const infoGainBonus = -Math.min (1.2, infoGain) * 4
|
||||
@@ -2050,7 +2044,7 @@ const chooseQuestion = (
|
||||
.filter ((item): item is {
|
||||
question: GekanatorQuestion
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -2135,15 +2129,15 @@ const chooseWinningRunQuestion = ({
|
||||
return false
|
||||
|
||||
const expected = expectedAnswerForQuestion (question, targetPost)
|
||||
return expected !== null && expected !== 'unknown'
|
||||
return expected != null && expected !== 'unknown'
|
||||
})
|
||||
.map (question => {
|
||||
const expected = expectedAnswerForQuestion (question, targetPost)
|
||||
const priority =
|
||||
expected === null
|
||||
expected == null
|
||||
? null
|
||||
: winningRunPriorityFor (expected)
|
||||
if (priority === null)
|
||||
if (priority == null)
|
||||
return null
|
||||
|
||||
const yesCount = matchingPostCountInIds ({
|
||||
@@ -2168,7 +2162,7 @@ const chooseWinningRunQuestion = ({
|
||||
question: GekanatorQuestion
|
||||
priority: number
|
||||
humanOffset: number
|
||||
matchingCount: number } => item !== null)
|
||||
matchingCount: number } => item != null)
|
||||
.sort ((a, b) => {
|
||||
if (a.priority !== b.priority)
|
||||
return a.priority - b.priority
|
||||
@@ -2258,7 +2252,7 @@ const chooseFallbackQuestion = ({
|
||||
question: GekanatorQuestion
|
||||
knownCount: number
|
||||
balance: number
|
||||
humanOffset: number } => item !== null)
|
||||
humanOffset: number } => item != null)
|
||||
.sort ((a, b) => {
|
||||
if (a.humanOffset !== b.humanOffset)
|
||||
return a.humanOffset - b.humanOffset
|
||||
@@ -2291,13 +2285,13 @@ const isWinningRunActive = (
|
||||
winningRunTargetId: number | null,
|
||||
winningRunStartAnswerCount: number | null,
|
||||
): boolean =>
|
||||
winningRunTargetId !== null && winningRunStartAnswerCount !== null
|
||||
winningRunTargetId != null && winningRunStartAnswerCount != null
|
||||
|
||||
|
||||
const winningRunQuestionCount = (
|
||||
answers: GekanatorAnswerLog[],
|
||||
winningRunStartAnswerCount: number | null,
|
||||
): number => winningRunStartAnswerCount === null
|
||||
): number => winningRunStartAnswerCount == null
|
||||
? 0
|
||||
: answers
|
||||
.slice (winningRunStartAnswerCount)
|
||||
@@ -2377,15 +2371,15 @@ const nextQuestionPlanFor = (
|
||||
? eligiblePosts[0]?.id ?? null
|
||||
: null
|
||||
const nextWinningRunStartAnswerCount =
|
||||
nextWinningRunTargetId === null
|
||||
nextWinningRunTargetId == null
|
||||
? null
|
||||
: ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
||||
&& winningRunTargetId === nextWinningRunTargetId
|
||||
&& winningRunStartAnswerCount !== null)
|
||||
&& winningRunStartAnswerCount != null)
|
||||
? winningRunStartAnswerCount
|
||||
: answers.length)
|
||||
const nextWinningRunTargetPost =
|
||||
nextWinningRunTargetId === null
|
||||
nextWinningRunTargetId == null
|
||||
? null
|
||||
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
|
||||
const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] =>
|
||||
@@ -2398,8 +2392,8 @@ const nextQuestionPlanFor = (
|
||||
if (eligiblePosts.length === 1)
|
||||
{
|
||||
const winningRunFinished =
|
||||
nextWinningRunTargetId !== null
|
||||
&& nextWinningRunStartAnswerCount !== null
|
||||
nextWinningRunTargetId != null
|
||||
&& nextWinningRunStartAnswerCount != null
|
||||
&& eligiblePosts[0]?.id === nextWinningRunTargetId
|
||||
&& winningRunQuestionCount (
|
||||
answers,
|
||||
@@ -2412,7 +2406,7 @@ const nextQuestionPlanFor = (
|
||||
questionMode: null,
|
||||
winningRunTargetId: nextWinningRunTargetId,
|
||||
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
|
||||
if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount === null)
|
||||
if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount == null)
|
||||
return {
|
||||
question: null,
|
||||
guess: null,
|
||||
@@ -2586,9 +2580,9 @@ const backgroundPostsFor = ({
|
||||
}): Post[] => {
|
||||
const focusPosts =
|
||||
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'
|
||||
? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post !== null)
|
||||
? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post != null)
|
||||
: eligiblePosts.length > 0
|
||||
? eligiblePosts
|
||||
: availablePosts
|
||||
@@ -2646,7 +2640,7 @@ const GekanatorBackdrop: FC<{
|
||||
const isWinningRunBackdrop =
|
||||
!(guessThumbnail)
|
||||
&& phase === 'question'
|
||||
&& winningRunTargetPost !== null
|
||||
&& winningRunTargetPost != null
|
||||
&& Boolean (backgroundThumbnailUrl (winningRunTargetPost))
|
||||
const backdropMode =
|
||||
guessThumbnail
|
||||
@@ -2843,7 +2837,7 @@ const GekanatorBackdrop: FC<{
|
||||
setActiveDirection (nextDirection)
|
||||
}
|
||||
|
||||
if (flipTimerRef.current !== null)
|
||||
if (flipTimerRef.current != null)
|
||||
{
|
||||
window.clearTimeout (flipTimerRef.current)
|
||||
flipTimerRef.current = null
|
||||
@@ -2924,7 +2918,7 @@ const GekanatorBackdrop: FC<{
|
||||
}, tileFlipDuration * 1000)
|
||||
|
||||
return () => {
|
||||
if (flipTimerRef.current !== null)
|
||||
if (flipTimerRef.current != null)
|
||||
{
|
||||
window.clearTimeout (flipTimerRef.current)
|
||||
flipTimerRef.current = null
|
||||
@@ -3072,10 +3066,10 @@ const expectedAnswerFor = (
|
||||
|
||||
const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
const storedGame = useMemo (loadStoredGame, [])
|
||||
const hasStoredRestore = storedGame !== null && isStoredPhase (storedGame.phase)
|
||||
const hasStoredRestore = storedGame != null && isStoredPhase (storedGame.phase)
|
||||
const queryClient = useQueryClient ()
|
||||
const isAdmin = user?.role === 'admin'
|
||||
const canPersistGame = user !== null
|
||||
const canPersistGame = user != null
|
||||
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
|
||||
() => loadRecentGames ())
|
||||
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
|
||||
@@ -3188,7 +3182,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
setAskedQuestionBank (
|
||||
storedAskedQuestionBankIds
|
||||
.map (questionId => questionById.get (questionId))
|
||||
.filter ((question): question is GekanatorQuestion => question !== undefined))
|
||||
.filter ((question): question is GekanatorQuestion => question != null))
|
||||
setStoredAskedQuestionBankIds ([])
|
||||
}, [posts,
|
||||
storedAskedQuestionBankIds,
|
||||
@@ -3340,11 +3334,11 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
?? null,
|
||||
[acceptedQuestions, questionSuggestionSelectedId])
|
||||
const canSubmitQuestionSuggestion = useMemo (() => {
|
||||
if (!(canPersistGame) || reviewCorrectPostId === null)
|
||||
if (!(canPersistGame) || reviewCorrectPostId == null)
|
||||
return false
|
||||
|
||||
if (questionSuggestionEntryMode === 'search')
|
||||
return selectedSuggestedQuestion !== null
|
||||
return selectedSuggestedQuestion != null
|
||||
|
||||
return questionSuggestion.trim () !== ''
|
||||
}, [
|
||||
@@ -3403,7 +3397,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
userPriorWeights, materialIndex, acceptedQuestionMatchIndex,
|
||||
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
||||
const winningRunTargetPost = useMemo (
|
||||
() => questionPlan.winningRunTargetId === null
|
||||
() => questionPlan.winningRunTargetId == null
|
||||
? null
|
||||
: posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null,
|
||||
[posts, questionPlan.winningRunTargetId])
|
||||
@@ -3417,7 +3411,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
&& winningRunQuestionsAsked < winningRunQuestionLimit
|
||||
&& eligiblePosts.length === 1
|
||||
&& eligiblePosts[0]?.id === questionPlan.winningRunTargetId
|
||||
&& winningRunTargetPost !== null
|
||||
&& winningRunTargetPost != null
|
||||
const topScoredPosts = useMemo (
|
||||
() => eligiblePosts
|
||||
.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
|
||||
const effectiveResultWon =
|
||||
resultWon
|
||||
?? ((reviewGuessedPostId !== null && reviewCorrectPostId !== null)
|
||||
?? ((reviewGuessedPostId != null && reviewCorrectPostId != null)
|
||||
? reviewGuessedPostId === reviewCorrectPostId
|
||||
: null)
|
||||
const effectiveBackgroundMotionMode =
|
||||
@@ -3895,7 +3889,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
}
|
||||
|
||||
const startReview = () => {
|
||||
if (reviewGuessedPostId === null || reviewCorrectPostId === null)
|
||||
if (reviewGuessedPostId == null || reviewCorrectPostId == null)
|
||||
return
|
||||
|
||||
saveMutation.reset ()
|
||||
@@ -3915,13 +3909,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
const saveReviewedResult = (onSuccess: (gameId: number) => void) => {
|
||||
if (
|
||||
!(canPersistGame)
|
||||
|| reviewGuessedPostId === null
|
||||
|| reviewCorrectPostId === null
|
||||
|| reviewGuessedPostId == null
|
||||
|| reviewCorrectPostId == null
|
||||
|| saveMutation.isPending
|
||||
)
|
||||
return
|
||||
|
||||
if (savedGameId !== null)
|
||||
if (savedGameId != null)
|
||||
{
|
||||
onSuccess (savedGameId)
|
||||
return
|
||||
@@ -3979,7 +3973,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
const saveExtraQuestions = () => {
|
||||
if (
|
||||
!(canPersistGame)
|
||||
|| savedGameId === null
|
||||
|| savedGameId == null
|
||||
|| extraQuestionAnswersMutation.isPending
|
||||
|| extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)]))
|
||||
)
|
||||
@@ -4175,7 +4169,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
}
|
||||
|
||||
const startExtraQuestions = () => {
|
||||
if (reviewCorrectPostId === null || saveMutation.isPending)
|
||||
if (reviewCorrectPostId == null || saveMutation.isPending)
|
||||
return
|
||||
|
||||
saveReviewedResult (gameId => {
|
||||
@@ -4206,7 +4200,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
const dialogue = phase === 'learned' ? resultDialogue : introDialogue
|
||||
const saveStatusMessage =
|
||||
saved
|
||||
&& learnedExampleCount !== null
|
||||
&& learnedExampleCount != null
|
||||
? learnedExampleCount > 0
|
||||
? `${ learnedExampleCount }件の回答を学習しました`
|
||||
: null
|
||||
@@ -4436,7 +4430,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
{' / '}
|
||||
recoveryStepCount: {recoveryStepCount}
|
||||
{' / '}
|
||||
currentQuestion===null: {String (currentQuestion === null)}
|
||||
currentQuestion===null: {String (currentQuestion == null)}
|
||||
</div>
|
||||
{topScoredPosts.length > 0 && (
|
||||
<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}
|
||||
{' / '}
|
||||
currentQuestion===null: {String (currentQuestion === null)}
|
||||
currentQuestion===null: {String (currentQuestion == null)}
|
||||
</div>)}
|
||||
<PostMiniCard post={displayedGuess}/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -4621,7 +4615,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
||||
{reviewGuessedPostId != null && reviewCorrectPostId != null && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
||||
</p>)}
|
||||
@@ -4648,7 +4642,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
type="button"
|
||||
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
|
||||
hover:bg-pink-500 disabled:opacity-50"
|
||||
disabled={reviewCorrectPostId === null || saveMutation.isPending}
|
||||
disabled={reviewCorrectPostId == null || saveMutation.isPending}
|
||||
onClick={saveAndReset}>
|
||||
もう一度
|
||||
</button>
|
||||
@@ -4659,7 +4653,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
dark:hover:bg-red-900 disabled:opacity-50"
|
||||
disabled={
|
||||
!(canPersistGame)
|
||||
|| reviewCorrectPostId === null
|
||||
|| reviewCorrectPostId == null
|
||||
|| saveMutation.isPending
|
||||
}
|
||||
onClick={startReview}>
|
||||
@@ -4682,7 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
hover:bg-yellow-100 dark:border-red-700
|
||||
dark:hover:bg-red-900 disabled:opacity-50"
|
||||
disabled={!(canPersistGame)
|
||||
|| reviewCorrectPostId === null
|
||||
|| reviewCorrectPostId == null
|
||||
|| saveMutation.isPending
|
||||
|| extraQuestionState === 'loading'
|
||||
|| extraQuestionAnswersMutation.isPending}
|
||||
@@ -4768,7 +4762,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
||||
{reviewGuessedPostId != null && reviewCorrectPostId != null && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||
判定:
|
||||
{reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
||||
@@ -4798,7 +4792,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
hover:bg-pink-500 disabled:opacity-50"
|
||||
disabled={
|
||||
!(canPersistGame)
|
||||
|| reviewCorrectPostId === null
|
||||
|| reviewCorrectPostId == null
|
||||
|| saveMutation.isPending
|
||||
|| questionSuggestionMutation.isPending
|
||||
}
|
||||
|
||||
新しい課題から参照
ユーザをブロックする