グカネータ作成 / ウィニング・ラン修正 (#41) #366

マージ済み
みてるぞ が 22 個のコミットを feature/041 から main へマージ 2026-06-12 02:08:59 +09:00
2個のファイルの変更172行の追加8行の削除
コミット 159ad5ed5a の変更だけを表示してゐます - すべてのコミットを表示
+4 -1
ファイルの表示
@@ -13,7 +13,10 @@ class GekanatorQuestion < ApplicationRecord
validates :condition, presence: true validates :condition, presence: true
validates :priority_weight, validates :priority_weight,
presence: true, presence: true,
numericality: { greater_than: 0 } numericality: {
greater_than: 0,
less_than_or_equal_to: 3
}
scope :accepted, -> { where(status: 'accepted') } scope :accepted, -> { where(status: 'accepted') }
end end
+168 -7
ファイルの表示
@@ -125,7 +125,7 @@ const sourcePriorityOffset = (question: GekanatorQuestion): number => {
const priorityWeightOffset = (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 => { 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 = ({ const chooseQuestion = ({
posts, posts,
questions, questions,
scores, scores,
answers,
askedIds, askedIds,
gameSeed, gameSeed,
}: { }: {
posts: Post[] posts: Post[]
questions: GekanatorQuestion[] questions: GekanatorQuestion[]
scores: Map<number, number> scores: Map<number, number>
answers: GekanatorAnswerLog[]
askedIds: Set<string> askedIds: Set<string>
gameSeed: string gameSeed: string
}): GekanatorQuestion | null => { }): GekanatorQuestion | null => {
@@ -527,6 +673,7 @@ const chooseQuestion = ({
const tagPenalty = question.kind === 'tag' && nonTagCount < 4 ? .12 : 0 const tagPenalty = question.kind === 'tag' && nonTagCount < 4 ? .12 : 0
const minSide = candidates.length < 10 ? 1 : Math.max (3, candidates.length * .08) const minSide = candidates.length < 10 ? 1 : Math.max (3, candidates.length * .08)
const narrowPenalty = yes < minSide || no < minSide ? .15 : 0 const narrowPenalty = yes < minSide || no < minSide ? .15 : 0
const contradictionPenalty = contradictionPenaltyFor ({ question, answers })
const sourceBonus = sourcePriorityOffset (question) const sourceBonus = sourcePriorityOffset (question)
const priorityBonus = priorityWeightOffset (question) const priorityBonus = priorityWeightOffset (question)
@@ -535,6 +682,7 @@ const chooseQuestion = ({
+ unweightedSplitScore * 8 + unweightedSplitScore * 8
+ tagPenalty + tagPenalty
+ narrowPenalty + narrowPenalty
+ contradictionPenalty
+ sourceBonus + sourceBonus
+ priorityBonus, + priorityBonus,
narrow: narrowPenalty > 0 } narrow: narrowPenalty > 0 }
@@ -666,11 +814,18 @@ const GekanatorPage: FC = () => {
const { data: posts = [], isLoading, error } = useQuery ({ const { data: posts = [], isLoading, error } = useQuery ({
queryKey: gekanatorKeys.posts (), queryKey: gekanatorKeys.posts (),
queryFn: fetchGekanatorPosts }) queryFn: fetchGekanatorPosts,
const { data: acceptedQuestions = [], isFetched: acceptedQuestionsFetched } = useQuery ({ refetchOnWindowFocus: false })
const {
data: acceptedQuestions = [],
isFetched: acceptedQuestionsFetched,
isLoading: acceptedQuestionsLoading,
error: acceptedQuestionsError
} = useQuery ({
queryKey: gekanatorKeys.questions (), queryKey: gekanatorKeys.questions (),
queryFn: fetchGekanatorQuestions, queryFn: fetchGekanatorQuestions,
select: questions => questions.map (restoreGekanatorQuestion) }) select: questions => questions.map (restoreGekanatorQuestion),
refetchOnWindowFocus: false })
useEffect (() => { useEffect (() => {
if ( if (
@@ -793,6 +948,7 @@ const GekanatorPage: FC = () => {
posts: questionPosts, posts: questionPosts,
questions: scoringQuestions, questions: scoringQuestions,
scores, scores,
answers,
askedIds, askedIds,
gameSeed }) gameSeed })
const answerPreviews = useMemo ( const answerPreviews = useMemo (
@@ -896,6 +1052,7 @@ const GekanatorPage: FC = () => {
posts: recoveredEligiblePosts, posts: recoveredEligiblePosts,
questions: recoveredScoringQuestions, questions: recoveredScoringQuestions,
scores: recoveredScores, scores: recoveredScores,
answers: nextAnswers,
askedIds: nextAskedIds, askedIds: nextAskedIds,
gameSeed }))) gameSeed })))
) )
@@ -1170,6 +1327,7 @@ const GekanatorPage: FC = () => {
: nonRejectedPosts, : nonRejectedPosts,
questions: recovered.scoringQuestions, questions: recovered.scoringQuestions,
scores: recovered.scores, scores: recovered.scores,
answers,
askedIds, askedIds,
gameSeed }) gameSeed })
@@ -1229,6 +1387,8 @@ const GekanatorPage: FC = () => {
phase === 'learned' && resultWon phase === 'learned' && resultWon
? <>wwwww <ruby>鹿<rt></rt></ruby>!</> ? <>wwwww <ruby>鹿<rt></rt></ruby>!</>
: <><ruby>鹿<rt></rt></ruby>稿</> : <><ruby>鹿<rt></rt></ruby>稿</>
const introLoading = isLoading || acceptedQuestionsLoading
const readyToStart = !(introLoading) && acceptedQuestionsFetched && posts.length > 0
return ( return (
<MainArea className="bg-yellow-50 dark:bg-red-975"> <MainArea className="bg-yellow-50 dark:bg-red-975">
@@ -1258,15 +1418,16 @@ const GekanatorPage: FC = () => {
{dialogue} {dialogue}
</p> </p>
{isLoading && ( {introLoading && (
<p> <p>
{phase === 'intro' {phase === 'intro'
? '投稿を読み込んでゐます...' ? '投稿を読み込んでゐます...'
: '前回のグカネータ状態を復元してゐます...'} : '前回のグカネータ状態を復元してゐます...'}
</p>)} </p>)}
{Boolean (error) && <p>稿</p>} {(Boolean (error) || Boolean (acceptedQuestionsError))
&& <p></p>}
{phase === 'intro' && !(isLoading) && posts.length > 0 && ( {phase === 'intro' && readyToStart && (
<button <button
type="button" type="button"
className="rounded bg-pink-600 px-4 py-2 font-bold text-white className="rounded bg-pink-600 px-4 py-2 font-bold text-white