#41 ウィニング・ラン修正
このコミットが含まれているのは:
@@ -78,6 +78,8 @@ type GameSnapshot = {
|
||||
rejectedPostIds: Set<number>
|
||||
lastGuessQuestionCount: number
|
||||
lastRejectedGuessId: number | null
|
||||
winningRunTargetId: number | null
|
||||
winningRunStartAnswerCount: number | null
|
||||
activeGuessId: number | null
|
||||
reviewGuessedPostId: number | null
|
||||
reviewCorrectPostId: number | null }
|
||||
@@ -99,6 +101,8 @@ type StoredGekanatorGame = {
|
||||
rejectedPostIds: number[]
|
||||
lastGuessQuestionCount: number
|
||||
lastRejectedGuessId: number | null
|
||||
winningRunTargetId?: number | null
|
||||
winningRunStartAnswerCount?: number | null
|
||||
activeGuessId: number | null
|
||||
reviewGuessedPostId: number | null
|
||||
reviewCorrectPostId: number | null
|
||||
@@ -126,6 +130,7 @@ const minQuestionsBeforeCertainGuess = 5
|
||||
const certainGuessPercent = 99.5
|
||||
const runnerUpMaxPercent = .5
|
||||
const hardMaxQuestions = 80
|
||||
const winningRunQuestionLimit = 3
|
||||
const softenedAnswerWeight = .35
|
||||
const confidenceTemperature = 6
|
||||
const gameStorageKey = 'gekanator:game:v1'
|
||||
@@ -191,6 +196,8 @@ const normalizeStoredGame = (game: StoredGekanatorGame): StoredGekanatorGame =>
|
||||
normalizeStoredQuestionId (questionId)),
|
||||
recoveredCandidatePosts: game.recoveredCandidatePosts ?? [],
|
||||
recoveryStepCount: game.recoveryStepCount ?? 0,
|
||||
winningRunTargetId: game.winningRunTargetId ?? null,
|
||||
winningRunStartAnswerCount: game.winningRunStartAnswerCount ?? null,
|
||||
askedQuestionBank: game.askedQuestionBank?.map (question =>
|
||||
({
|
||||
...question,
|
||||
@@ -828,6 +835,135 @@ const chooseQuestion = ({
|
||||
}
|
||||
|
||||
|
||||
const directWinningRunExampleAnswerFor = (
|
||||
question: GekanatorQuestion,
|
||||
targetPost: Post,
|
||||
): GekanatorAnswerValue | null =>
|
||||
question.kind !== 'post_similarity'
|
||||
? null
|
||||
: question.exampleAnswers?.[String (targetPost.id) as `${ number }`] ?? null
|
||||
|
||||
|
||||
const winningRunPriorityFor = (
|
||||
question: GekanatorQuestion,
|
||||
expected: GekanatorAnswerValue,
|
||||
targetPost: Post,
|
||||
): number | null => {
|
||||
if (question.kind === 'post_similarity')
|
||||
{
|
||||
const directAnswer = directWinningRunExampleAnswerFor (question, targetPost)
|
||||
if (directAnswer === null)
|
||||
return null
|
||||
if (expected === 'yes')
|
||||
return 1
|
||||
if (expected === 'no')
|
||||
return 3
|
||||
return null
|
||||
}
|
||||
|
||||
if (expected === 'yes')
|
||||
return 0
|
||||
if (expected === 'no')
|
||||
return 2
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const chooseWinningRunQuestion = ({
|
||||
posts,
|
||||
fallbackPosts,
|
||||
questions,
|
||||
targetPost,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed,
|
||||
}: {
|
||||
posts: Post[]
|
||||
fallbackPosts: Post[]
|
||||
questions: GekanatorQuestion[]
|
||||
targetPost: Post
|
||||
scores: Map<number, number>
|
||||
answers: GekanatorAnswerLog[]
|
||||
askedIds: Set<string>
|
||||
gameSeed: string
|
||||
}): GekanatorQuestion | null => {
|
||||
const ranked = questions
|
||||
.filter (question => {
|
||||
if (askedIds.has (question.id))
|
||||
return false
|
||||
if (isQuestionHardFilteredAfterAnswers (question, answers))
|
||||
return false
|
||||
|
||||
const expected = expectedAnswerForQuestion (question, targetPost)
|
||||
return expected !== null && expected !== 'unknown'
|
||||
})
|
||||
.map (question => {
|
||||
const expected = expectedAnswerForQuestion (question, targetPost)
|
||||
const priority =
|
||||
expected === null
|
||||
? null
|
||||
: winningRunPriorityFor (question, expected, targetPost)
|
||||
if (priority === null)
|
||||
return null
|
||||
|
||||
const yesCount = posts.filter (post => question.test (post)).length
|
||||
const matchingCount =
|
||||
expected === 'yes' || expected === 'partial'
|
||||
? yesCount
|
||||
: posts.length - yesCount
|
||||
|
||||
return {
|
||||
question,
|
||||
priority,
|
||||
matchingCount }
|
||||
})
|
||||
.filter ((item): item is {
|
||||
question: GekanatorQuestion
|
||||
priority: number
|
||||
matchingCount: number } => item !== null)
|
||||
.sort ((a, b) => {
|
||||
if (a.priority !== b.priority)
|
||||
return a.priority - b.priority
|
||||
|
||||
if (a.matchingCount !== b.matchingCount)
|
||||
return a.matchingCount - b.matchingCount
|
||||
|
||||
if (a.question.priorityWeight !== b.question.priorityWeight)
|
||||
return b.question.priorityWeight - a.question.priorityWeight
|
||||
|
||||
return a.question.id.localeCompare (b.question.id)
|
||||
})
|
||||
|
||||
if (ranked.length > 0)
|
||||
return ranked[0]?.question ?? null
|
||||
|
||||
return chooseQuestion ({
|
||||
posts: fallbackPosts.length > 0 ? fallbackPosts : posts,
|
||||
questions,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed })
|
||||
}
|
||||
|
||||
|
||||
const isWinningRunActive = (
|
||||
winningRunTargetId: number | null,
|
||||
winningRunStartAnswerCount: number | null,
|
||||
): boolean =>
|
||||
winningRunTargetId !== null && winningRunStartAnswerCount !== null
|
||||
|
||||
|
||||
const winningRunQuestionCount = (
|
||||
answers: GekanatorAnswerLog[],
|
||||
winningRunStartAnswerCount: number | null,
|
||||
): number => winningRunStartAnswerCount === null
|
||||
? 0
|
||||
: Math.max (0, answers.length - winningRunStartAnswerCount)
|
||||
|
||||
|
||||
const bestPost = (posts: Post[], scores: Map<number, number>): Post | null =>
|
||||
posts
|
||||
.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
|
||||
@@ -896,6 +1032,10 @@ const GekanatorPage: FC = () => {
|
||||
storedGame?.lastGuessQuestionCount ?? 0)
|
||||
const [lastRejectedGuessId, setLastRejectedGuessId] = useState<number | null> (
|
||||
storedGame?.lastRejectedGuessId ?? null)
|
||||
const [winningRunTargetId, setWinningRunTargetId] = useState<number | null> (
|
||||
storedGame?.winningRunTargetId ?? null)
|
||||
const [winningRunStartAnswerCount, setWinningRunStartAnswerCount] =
|
||||
useState<number | null> (storedGame?.winningRunStartAnswerCount ?? null)
|
||||
const [activeGuessId, setActiveGuessId] = useState<number | null> (
|
||||
storedGame?.activeGuessId ?? null)
|
||||
const [reviewGuessedPostId, setReviewGuessedPostId] = useState<number | null> (
|
||||
@@ -979,6 +1119,8 @@ const GekanatorPage: FC = () => {
|
||||
rejectedPostIds: [...rejectedPostIds],
|
||||
lastGuessQuestionCount,
|
||||
lastRejectedGuessId,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount,
|
||||
activeGuessId,
|
||||
reviewGuessedPostId,
|
||||
reviewCorrectPostId,
|
||||
@@ -1016,6 +1158,8 @@ const GekanatorPage: FC = () => {
|
||||
rejectedPostIds,
|
||||
lastGuessQuestionCount,
|
||||
lastRejectedGuessId,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount,
|
||||
activeGuessId,
|
||||
reviewGuessedPostId,
|
||||
reviewCorrectPostId,
|
||||
@@ -1053,6 +1197,20 @@ const GekanatorPage: FC = () => {
|
||||
const availablePosts = useMemo (
|
||||
() => posts.filter (post => !(rejectedPostIds.has (post.id))),
|
||||
[posts, rejectedPostIds])
|
||||
const winningRunTargetPost = useMemo (
|
||||
() => winningRunTargetId === null
|
||||
? null
|
||||
: posts.find (post => post.id === winningRunTargetId) ?? null,
|
||||
[posts, winningRunTargetId])
|
||||
const winningRunQuestionsAsked = winningRunQuestionCount (
|
||||
answers,
|
||||
winningRunStartAnswerCount)
|
||||
const winningRunActive =
|
||||
isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
||||
&& winningRunQuestionsAsked < winningRunQuestionLimit
|
||||
&& eligiblePosts.length === 1
|
||||
&& eligiblePosts[0]?.id === winningRunTargetId
|
||||
&& winningRunTargetPost !== null
|
||||
const questionPosts =
|
||||
eligiblePosts.length > 1
|
||||
|| questionsSinceLastGuess >= minQuestionsBeforeCertainGuess
|
||||
@@ -1064,13 +1222,23 @@ const GekanatorPage: FC = () => {
|
||||
.sort ((a, b) => b.score - a.score)
|
||||
.slice (0, 3),
|
||||
[eligiblePosts, scores])
|
||||
const currentQuestion = chooseQuestion ({
|
||||
posts: questionPosts,
|
||||
questions: scoringQuestions,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed })
|
||||
const currentQuestion = winningRunActive && winningRunTargetPost
|
||||
? chooseWinningRunQuestion ({
|
||||
posts,
|
||||
fallbackPosts: availablePosts.length > 1 ? availablePosts : posts,
|
||||
questions: scoringQuestions,
|
||||
targetPost: winningRunTargetPost,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed })
|
||||
: chooseQuestion ({
|
||||
posts: questionPosts,
|
||||
questions: scoringQuestions,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed })
|
||||
const answerPreviews = useMemo (
|
||||
() => currentQuestion
|
||||
? answerOptions.map (option => previewAnswer ({
|
||||
@@ -1141,6 +1309,8 @@ const GekanatorPage: FC = () => {
|
||||
setRejectedPostIds (new Set ())
|
||||
setLastGuessQuestionCount (0)
|
||||
setLastRejectedGuessId (null)
|
||||
setWinningRunTargetId (null)
|
||||
setWinningRunStartAnswerCount (null)
|
||||
setActiveGuessId (null)
|
||||
setReviewGuessedPostId (null)
|
||||
setReviewCorrectPostId (null)
|
||||
@@ -1299,6 +1469,8 @@ const GekanatorPage: FC = () => {
|
||||
rejectedPostIds: new Set (rejectedPostIds),
|
||||
lastGuessQuestionCount,
|
||||
lastRejectedGuessId,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount,
|
||||
activeGuessId,
|
||||
reviewGuessedPostId,
|
||||
reviewCorrectPostId }])
|
||||
@@ -1324,6 +1496,20 @@ const GekanatorPage: FC = () => {
|
||||
const nextRecoveredCandidatePosts = recovered.recoveredCandidatePosts
|
||||
const nextScores = recovered.scores
|
||||
const nextEligiblePosts = recovered.eligiblePosts
|
||||
const currentWinningRunActive =
|
||||
isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
||||
const nextWinningRunTargetId =
|
||||
nextEligiblePosts.length === 1
|
||||
? nextEligiblePosts[0]?.id ?? null
|
||||
: null
|
||||
const nextWinningRunStartAnswerCount =
|
||||
nextWinningRunTargetId === null
|
||||
? null
|
||||
: currentWinningRunActive
|
||||
&& winningRunTargetId === nextWinningRunTargetId
|
||||
&& winningRunStartAnswerCount !== null
|
||||
? winningRunStartAnswerCount
|
||||
: nextAnswers.length
|
||||
|
||||
setScores (nextScores)
|
||||
setAskedIds (nextAskedIds)
|
||||
@@ -1332,6 +1518,8 @@ const GekanatorPage: FC = () => {
|
||||
setRecoveryStepCount (recovered.recoveryStepCount)
|
||||
setAskedQuestionBank (nextAskedQuestionBank)
|
||||
setAnswers (nextAnswers)
|
||||
setWinningRunTargetId (nextWinningRunTargetId)
|
||||
setWinningRunStartAnswerCount (nextWinningRunStartAnswerCount)
|
||||
|
||||
const nextGuessablePosts =
|
||||
nextEligiblePosts.length > 0
|
||||
@@ -1345,6 +1533,13 @@ const GekanatorPage: FC = () => {
|
||||
const topConfidence = nextConfidences[0] ?? null
|
||||
const runnerUpConfidence = nextConfidences[1] ?? null
|
||||
const structurallyCertain = nextEligiblePosts.length === 1
|
||||
const winningRunContinues =
|
||||
nextWinningRunTargetId !== null
|
||||
&& nextWinningRunStartAnswerCount !== null
|
||||
&& nextEligiblePosts.length === 1
|
||||
&& winningRunQuestionCount (
|
||||
nextAnswers,
|
||||
nextWinningRunStartAnswerCount) < winningRunQuestionLimit
|
||||
const statisticallyCertain =
|
||||
topConfidence !== null
|
||||
&& topConfidence.percent >= certainGuessPercent
|
||||
@@ -1357,8 +1552,9 @@ const GekanatorPage: FC = () => {
|
||||
&& (structurallyCertain || statisticallyCertain)
|
||||
const shouldGuess =
|
||||
nextQuestionCount >= hardMaxQuestions
|
||||
|| canGuessByQuestionCount
|
||||
|| canGuessEarlyByConfidence
|
||||
|| (
|
||||
!(winningRunContinues)
|
||||
&& (canGuessByQuestionCount || canGuessEarlyByConfidence))
|
||||
if (shouldGuess)
|
||||
{
|
||||
setActiveGuessId (nextGuess?.id ?? null)
|
||||
@@ -1476,6 +1672,8 @@ const GekanatorPage: FC = () => {
|
||||
new Map (
|
||||
[...recoveredCandidatePosts.entries ()].filter (
|
||||
([postId]) => postId !== displayedGuess.id)))
|
||||
setWinningRunTargetId (null)
|
||||
setWinningRunStartAnswerCount (null)
|
||||
setActiveGuessId (null)
|
||||
setSearch ('')
|
||||
setSelectingCorrectPost (false)
|
||||
@@ -1501,6 +1699,8 @@ const GekanatorPage: FC = () => {
|
||||
setRejectedPostIds (snapshot.rejectedPostIds)
|
||||
setLastGuessQuestionCount (snapshot.lastGuessQuestionCount)
|
||||
setLastRejectedGuessId (snapshot.lastRejectedGuessId)
|
||||
setWinningRunTargetId (snapshot.winningRunTargetId)
|
||||
setWinningRunStartAnswerCount (snapshot.winningRunStartAnswerCount)
|
||||
setActiveGuessId (snapshot.activeGuessId)
|
||||
setReviewGuessedPostId (snapshot.reviewGuessedPostId)
|
||||
setReviewCorrectPostId (snapshot.reviewCorrectPostId)
|
||||
@@ -1525,20 +1725,54 @@ const GekanatorPage: FC = () => {
|
||||
setRecoveredCandidatePosts (recovered.recoveredCandidatePosts)
|
||||
setRecoveryStepCount (recovered.recoveryStepCount)
|
||||
setScores (recovered.scores)
|
||||
const nextWinningRunTargetId =
|
||||
recovered.eligiblePosts.length === 1
|
||||
? recovered.eligiblePosts[0]?.id ?? null
|
||||
: null
|
||||
const nextWinningRunStartAnswerCount =
|
||||
nextWinningRunTargetId === null
|
||||
? null
|
||||
: isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
||||
&& winningRunTargetId === nextWinningRunTargetId
|
||||
&& winningRunStartAnswerCount !== null
|
||||
? winningRunStartAnswerCount
|
||||
: answers.length
|
||||
const nextWinningRunTargetPost =
|
||||
nextWinningRunTargetId === null
|
||||
? null
|
||||
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
|
||||
|
||||
const recoveredGuessablePosts =
|
||||
recovered.eligiblePosts.length > 0
|
||||
? recovered.eligiblePosts
|
||||
: availablePosts
|
||||
const nextQuestion = chooseQuestion ({
|
||||
posts: recovered.eligiblePosts.length > 1
|
||||
? recovered.eligiblePosts
|
||||
: availablePosts,
|
||||
questions: recovered.scoringQuestions,
|
||||
scores: recovered.scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed })
|
||||
const nextQuestion =
|
||||
nextWinningRunTargetPost
|
||||
&& nextWinningRunStartAnswerCount !== null
|
||||
&& winningRunQuestionCount (
|
||||
answers,
|
||||
nextWinningRunStartAnswerCount) < winningRunQuestionLimit
|
||||
? chooseWinningRunQuestion ({
|
||||
posts,
|
||||
fallbackPosts: availablePosts.length > 1 ? availablePosts : posts,
|
||||
questions: recovered.scoringQuestions,
|
||||
targetPost: nextWinningRunTargetPost,
|
||||
scores: recovered.scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed })
|
||||
: chooseQuestion ({
|
||||
posts: recovered.eligiblePosts.length > 1
|
||||
? recovered.eligiblePosts
|
||||
: availablePosts,
|
||||
questions: recovered.scoringQuestions,
|
||||
scores: recovered.scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed })
|
||||
|
||||
setWinningRunTargetId (nextWinningRunTargetId)
|
||||
setWinningRunStartAnswerCount (nextWinningRunStartAnswerCount)
|
||||
|
||||
if (nextQuestion)
|
||||
{
|
||||
@@ -1661,6 +1895,39 @@ const GekanatorPage: FC = () => {
|
||||
&& !(error)
|
||||
&& !(acceptedQuestionsError)
|
||||
|
||||
useEffect (() => {
|
||||
if (
|
||||
phase !== 'question'
|
||||
|| isLoading
|
||||
|| acceptedQuestionsLoading
|
||||
)
|
||||
return
|
||||
|
||||
const winningRunFinished =
|
||||
winningRunTargetId !== null
|
||||
&& winningRunStartAnswerCount !== null
|
||||
&& winningRunQuestionCount (answers, winningRunStartAnswerCount) >= winningRunQuestionLimit
|
||||
&& eligiblePosts.length === 1
|
||||
&& eligiblePosts[0]?.id === winningRunTargetId
|
||||
const nextGuess = displayedGuess ?? guess
|
||||
if (currentQuestion || (!(winningRunFinished) && !(nextGuess)))
|
||||
return
|
||||
|
||||
setActiveGuessId ((winningRunFinished ? guess : nextGuess)?.id ?? null)
|
||||
setLastGuessQuestionCount (answers.length)
|
||||
setPhase ('guess')
|
||||
}, [
|
||||
phase,
|
||||
currentQuestion,
|
||||
guess,
|
||||
displayedGuess,
|
||||
answers,
|
||||
eligiblePosts,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount,
|
||||
isLoading,
|
||||
acceptedQuestionsLoading])
|
||||
|
||||
return (
|
||||
<MainArea className="bg-yellow-50 dark:bg-red-975">
|
||||
<Helmet>
|
||||
@@ -1767,24 +2034,6 @@ const GekanatorPage: FC = () => {
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{!(isLoading) && phase === 'question' && !(currentQuestion) && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xl font-bold">
|
||||
もう十分わかった。
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-yellow-300 px-4 py-2
|
||||
hover:bg-yellow-100 dark:border-red-700
|
||||
dark:hover:bg-red-900"
|
||||
onClick={() => {
|
||||
setActiveGuessId (guess?.id ?? null)
|
||||
setPhase ('guess')
|
||||
}}>
|
||||
答える
|
||||
</button>
|
||||
</div>)}
|
||||
|
||||
{phase === 'guess' && displayedGuess && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xl font-bold">これを想像してゐたね?</p>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする