グカネータ作成 (#041) #362

マージ済み
みてるぞ が 13 個のコミットを feature/041 から main へマージ 2026-06-10 23:33:57 +09:00
2個のファイルの変更172行の追加8行の削除
コミット 159ad5ed5a の変更だけを表示してゐます - すべてのコミットを表示
+4 -1
ファイルの表示
@@ -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
+168 -7
ファイルの表示
@@ -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<number, number>
answers: GekanatorAnswerLog[]
askedIds: Set<string>
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 <ruby>鹿<rt></rt></ruby>!</>
: <><ruby>鹿<rt></rt></ruby>稿</>
const introLoading = isLoading || acceptedQuestionsLoading
const readyToStart = !(introLoading) && acceptedQuestionsFetched && posts.length > 0
return (
<MainArea className="bg-yellow-50 dark:bg-red-975">
@@ -1258,15 +1418,16 @@ const GekanatorPage: FC = () => {
{dialogue}
</p>
{isLoading && (
{introLoading && (
<p>
{phase === 'intro'
? '投稿を読み込んでゐます...'
: '前回のグカネータ状態を復元してゐます...'}
</p>)}
{Boolean (error) && <p>稿</p>}
{(Boolean (error) || Boolean (acceptedQuestionsError))
&& <p></p>}
{phase === 'intro' && !(isLoading) && posts.length > 0 && (
{phase === 'intro' && readyToStart && (
<button
type="button"
className="rounded bg-pink-600 px-4 py-2 font-bold text-white