From 159ad5ed5af030474f5a1fdb08056623bef0ed49 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 9 Jun 2026 23:36:24 +0900 Subject: [PATCH] #41 --- backend/app/models/gekanator_question.rb | 5 +- frontend/src/pages/GekanatorPage.tsx | 175 ++++++++++++++++++++++- 2 files changed, 172 insertions(+), 8 deletions(-) diff --git a/backend/app/models/gekanator_question.rb b/backend/app/models/gekanator_question.rb index a4c838f..8f9b925 100644 --- a/backend/app/models/gekanator_question.rb +++ b/backend/app/models/gekanator_question.rb @@ -13,7 +13,10 @@ class GekanatorQuestion < ApplicationRecord validates :condition, presence: true validates :priority_weight, presence: true, - numericality: { greater_than: 0 } + numericality: { + greater_than: 0, + less_than_or_equal_to: 3 + } scope :accepted, -> { where(status: 'accepted') } end diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 7a42884..2eef5b6 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -125,7 +125,7 @@ const sourcePriorityOffset = (question: GekanatorQuestion): number => { const priorityWeightOffset = (question: GekanatorQuestion): number => - (question.priorityWeight - 1) * -.8 + (Math.min (3, Math.max (.2, question.priorityWeight)) - 1) * -.8 const createGameSeed = (): string => { @@ -447,16 +447,162 @@ const softenNextQuestionIds = ({ } +type ExclusiveConditionGroup = + | 'original-month' + | 'original-year' + | 'original-month-day' + | 'source' + + +const exclusiveConditionGroupFor = ( + condition: GekanatorQuestion['condition'], +): ExclusiveConditionGroup | null => { + switch (condition.type) + { + case 'original-month': + return 'original-month' + case 'original-year': + return 'original-year' + case 'original-month-day': + return 'original-month-day' + case 'source': + return 'source' + default: + return null + } +} + + +const sameConditionValue = ( + left: GekanatorQuestion['condition'], + right: GekanatorQuestion['condition'], +): boolean => { + if (left.type !== right.type) + return false + + const valueKeyFor = (condition: GekanatorQuestion['condition']): string => { + switch (condition.type) + { + case 'tag': + return condition.key + case 'source': + return condition.host + case 'original-year': + return String (condition.year) + case 'original-month': + return String (condition.month) + case 'original-month-day': + return condition.monthDay + case 'title-length-greater-than': + return String (condition.length) + case 'title-has-ascii': + return '' + } + } + + return valueKeyFor (left) === valueKeyFor (right) +} + + +const monthForCondition = ( + condition: GekanatorQuestion['condition'], +): number | null => { + if (condition.type === 'original-month') + return condition.month + + if (condition.type !== 'original-month-day') + return null + + const month = Number (condition.monthDay.split ('-')[0]) + return Number.isInteger (month) ? month : null +} + + +const isMonthCrossMatch = ( + candidate: GekanatorQuestion['condition'], + previous: GekanatorQuestion['condition'], +): boolean => { + const candidateMonth = monthForCondition (candidate) + const previousMonth = monthForCondition (previous) + if (candidateMonth === null || previousMonth === null) + return false + + const sameType = candidate.type === previous.type + if (sameType) + return false + + return candidateMonth === previousMonth +} + + +const isExclusiveContradiction = ( + candidate: GekanatorQuestion['condition'], + previous: GekanatorQuestion['condition'], +): boolean => { + const candidateGroup = exclusiveConditionGroupFor (candidate) + const previousGroup = exclusiveConditionGroupFor (previous) + + if (candidateGroup !== null && candidateGroup === previousGroup) + return !(sameConditionValue (candidate, previous)) + + const candidateMonth = monthForCondition (candidate) + const previousMonth = monthForCondition (previous) + if (candidateMonth !== null && previousMonth !== null) + return candidateMonth !== previousMonth + + return false +} + + +const contradictionPenaltyFor = ({ + question, + answers, +}: { + question: GekanatorQuestion + answers: GekanatorAnswerLog[] +}): number => { + return answers.reduce ((sum, answer) => { + const previous = answer.questionCondition + if (!(previous)) + return sum + + switch (answer.answer) + { + case 'yes': + return sum + (isExclusiveContradiction (question.condition, previous) ? 100 : 0) + case 'partial': + return sum + (isExclusiveContradiction (question.condition, previous) ? 25 : 0) + case 'no': + return sum + ( + sameConditionValue (question.condition, previous) + || isMonthCrossMatch (question.condition, previous) + ? 40 + : 0) + case 'probably_no': + return sum + ( + sameConditionValue (question.condition, previous) + || isMonthCrossMatch (question.condition, previous) + ? 20 + : 0) + default: + return sum + } + }, 0) +} + + const chooseQuestion = ({ posts, questions, scores, + answers, askedIds, gameSeed, }: { posts: Post[] questions: GekanatorQuestion[] scores: Map + answers: GekanatorAnswerLog[] askedIds: Set gameSeed: string }): GekanatorQuestion | null => { @@ -527,6 +673,7 @@ const chooseQuestion = ({ const tagPenalty = question.kind === 'tag' && nonTagCount < 4 ? .12 : 0 const minSide = candidates.length < 10 ? 1 : Math.max (3, candidates.length * .08) const narrowPenalty = yes < minSide || no < minSide ? .15 : 0 + const contradictionPenalty = contradictionPenaltyFor ({ question, answers }) const sourceBonus = sourcePriorityOffset (question) const priorityBonus = priorityWeightOffset (question) @@ -535,6 +682,7 @@ const chooseQuestion = ({ + unweightedSplitScore * 8 + tagPenalty + narrowPenalty + + contradictionPenalty + sourceBonus + priorityBonus, narrow: narrowPenalty > 0 } @@ -666,11 +814,18 @@ const GekanatorPage: FC = () => { const { data: posts = [], isLoading, error } = useQuery ({ queryKey: gekanatorKeys.posts (), - queryFn: fetchGekanatorPosts }) - const { data: acceptedQuestions = [], isFetched: acceptedQuestionsFetched } = useQuery ({ + queryFn: fetchGekanatorPosts, + refetchOnWindowFocus: false }) + const { + data: acceptedQuestions = [], + isFetched: acceptedQuestionsFetched, + isLoading: acceptedQuestionsLoading, + error: acceptedQuestionsError + } = useQuery ({ queryKey: gekanatorKeys.questions (), queryFn: fetchGekanatorQuestions, - select: questions => questions.map (restoreGekanatorQuestion) }) + select: questions => questions.map (restoreGekanatorQuestion), + refetchOnWindowFocus: false }) useEffect (() => { if ( @@ -793,6 +948,7 @@ const GekanatorPage: FC = () => { posts: questionPosts, questions: scoringQuestions, scores, + answers, askedIds, gameSeed }) const answerPreviews = useMemo ( @@ -896,6 +1052,7 @@ const GekanatorPage: FC = () => { posts: recoveredEligiblePosts, questions: recoveredScoringQuestions, scores: recoveredScores, + answers: nextAnswers, askedIds: nextAskedIds, gameSeed }))) ) @@ -1170,6 +1327,7 @@ const GekanatorPage: FC = () => { : nonRejectedPosts, questions: recovered.scoringQuestions, scores: recovered.scores, + answers, askedIds, gameSeed }) @@ -1229,6 +1387,8 @@ const GekanatorPage: FC = () => { phase === 'learned' && resultWon ? <>グカカカカwwwww 洗澡鹿シーザオグカは何でもお見通し! : <>私は洗澡鹿シーザオグカ.質問から投稿を何でも当ててみせるよ + const introLoading = isLoading || acceptedQuestionsLoading + const readyToStart = !(introLoading) && acceptedQuestionsFetched && posts.length > 0 return ( @@ -1258,15 +1418,16 @@ const GekanatorPage: FC = () => { {dialogue}

- {isLoading && ( + {introLoading && (

{phase === 'intro' ? '投稿を読み込んでゐます...' : '前回のグカネータ状態を復元してゐます...'}

)} - {Boolean (error) &&

投稿を読み込めませんでした.

} + {(Boolean (error) || Boolean (acceptedQuestionsError)) + &&

グカネータの質問データを読み込めませんでした.

} - {phase === 'intro' && !(isLoading) && posts.length > 0 && ( + {phase === 'intro' && readyToStart && (