グカネータ作成 (#041) #362
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする