このコミットが含まれているのは:
@@ -158,6 +158,13 @@ npm run preview
|
||||
- Keep page-level code under `frontend/src/pages` and shared UI/feature code
|
||||
under `frontend/src/components` unless existing patterns point elsewhere.
|
||||
- Match existing Tailwind, component, and import alias conventions.
|
||||
- In TypeScript and TSX, prefer direct comparison operators such as `===` and
|
||||
`!==` over negating a comparison like `!(a === b)`.
|
||||
- In TypeScript and TSX, prefer `++i` or `--i` over `i += 1` or `i -= 1` for
|
||||
simple unit-step counter updates.
|
||||
- For user-facing Japanese text, prefer modern kana usage and natural current
|
||||
phrasing over historical spellings or awkward literal wording.
|
||||
- For user-facing Japanese ellipses, prefer `……` over ASCII `...`.
|
||||
|
||||
### Frontend TSX style
|
||||
|
||||
@@ -179,6 +186,9 @@ npm run preview
|
||||
single physical line.
|
||||
- Always add braces around `if`, `else`, or `for` bodies when the body spans
|
||||
two or more physical lines, even if it is one statement.
|
||||
- Do not use a leading semicolon for expression statements such as
|
||||
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
|
||||
explicitly, for example with `void`.
|
||||
|
||||
Preferred:
|
||||
|
||||
|
||||
+248
-229
@@ -123,14 +123,18 @@ type RecentGameSummary = {
|
||||
savedAt: number }
|
||||
|
||||
type BackgroundMotionMode = 'on' | 'calm' | 'off'
|
||||
|
||||
type GuessReason =
|
||||
| 'hard_max_questions'
|
||||
| 'winning_run_finished'
|
||||
| 'question_count_checkpoint'
|
||||
| 'question_generation_stalled'
|
||||
|
||||
type QuestionMode =
|
||||
| 'winning_run'
|
||||
| 'normal'
|
||||
| null
|
||||
|
||||
type MascotState =
|
||||
| 'idle'
|
||||
| 'thinking_far'
|
||||
@@ -214,9 +218,9 @@ const normalizeStoredQuestionId = (
|
||||
|
||||
if (questionId.startsWith ('title:length-greater-than:'))
|
||||
{
|
||||
const length = Number (questionId.split (':').pop ())
|
||||
if (Number.isInteger (length))
|
||||
return `title:length-at-least:${ length + 1 }`
|
||||
const length = Number (questionId.split (':').pop ())
|
||||
if (Number.isInteger (length))
|
||||
return `title:length-at-least:${ length + 1 }`
|
||||
}
|
||||
|
||||
return questionId
|
||||
@@ -227,30 +231,27 @@ const normalizeStoredGame = (game: StoredGekanatorGame): StoredGekanatorGame =>
|
||||
...game,
|
||||
answers: game.answers.map (answer => ({
|
||||
...answer,
|
||||
questionId: normalizeStoredQuestionId (
|
||||
answer.questionId,
|
||||
answer.questionCondition),
|
||||
questionMode:
|
||||
answer.questionMode === 'winning_run' || answer.questionMode === 'normal'
|
||||
? answer.questionMode
|
||||
: undefined,
|
||||
questionCondition: answer.questionCondition
|
||||
? normalizeTitleLengthCondition (answer.questionCondition)
|
||||
: undefined })),
|
||||
questionId: normalizeStoredQuestionId (answer.questionId,
|
||||
answer.questionCondition),
|
||||
questionMode: ((answer.questionMode === 'winning_run' || answer.questionMode === 'normal')
|
||||
? answer.questionMode
|
||||
: undefined),
|
||||
questionCondition: (answer.questionCondition
|
||||
? normalizeTitleLengthCondition (answer.questionCondition)
|
||||
: undefined) })),
|
||||
askedIds: game.askedIds.map (questionId => normalizeStoredQuestionId (questionId)),
|
||||
softenedQuestionIds: game.softenedQuestionIds.map (questionId =>
|
||||
normalizeStoredQuestionId (questionId)),
|
||||
softenedQuestionIds: (game.softenedQuestionIds
|
||||
.map (questionId => normalizeStoredQuestionId (questionId))),
|
||||
recoveredCandidatePosts: game.recoveredCandidatePosts ?? [],
|
||||
recoveryStepCount: game.recoveryStepCount ?? 0,
|
||||
winningRunTargetId: game.winningRunTargetId ?? null,
|
||||
winningRunStartAnswerCount: game.winningRunStartAnswerCount ?? null,
|
||||
askedQuestionBank: game.askedQuestionBank?.map (question =>
|
||||
({
|
||||
...question,
|
||||
id: normalizeStoredQuestionId (question.id, question.condition),
|
||||
condition: normalizeTitleLengthCondition (question.condition) })),
|
||||
askedQuestionBankIds: game.askedQuestionBankIds?.map (questionId =>
|
||||
normalizeStoredQuestionId (questionId)) })
|
||||
askedQuestionBank: game.askedQuestionBank?.map (question => ({
|
||||
...question,
|
||||
id: normalizeStoredQuestionId (question.id, question.condition),
|
||||
condition: normalizeTitleLengthCondition (question.condition) })),
|
||||
askedQuestionBankIds: (
|
||||
game.askedQuestionBankIds?.map (questionId => normalizeStoredQuestionId (questionId))) })
|
||||
|
||||
|
||||
const sourcePriorityForMerge = (question: GekanatorQuestion): number => {
|
||||
@@ -290,10 +291,10 @@ const shouldReplaceMergedQuestion = (
|
||||
const hashString = (value: string): number => {
|
||||
let hash = 2166136261
|
||||
|
||||
for (let i = 0; i < value.length; i += 1)
|
||||
for (let i = 0; i < value.length; ++i)
|
||||
{
|
||||
hash ^= value.charCodeAt (i)
|
||||
hash = Math.imul (hash, 16777619)
|
||||
hash ^= value.charCodeAt (i)
|
||||
hash = Math.imul (hash, 16777619)
|
||||
}
|
||||
|
||||
return hash >>> 0
|
||||
@@ -694,7 +695,7 @@ const buildMaterialIndex = (
|
||||
|
||||
const tagKeys = post.tags
|
||||
.filter (tag =>
|
||||
!(tag.category === 'meta')
|
||||
tag.category !== 'meta'
|
||||
&& !(tag.name.includes ('タグ希望'))
|
||||
&& !(tag.name.includes ('bot操作')))
|
||||
.map (tag => `${ tag.category }:${ tag.name }`)
|
||||
@@ -781,17 +782,17 @@ const indexedQuestionTextForTag = (key: string): string => {
|
||||
switch (category)
|
||||
{
|
||||
case 'deerjikist':
|
||||
return `作者・ニジラーとして「${ label }」に関係してゐる?`
|
||||
return `ニジラーとして「${ label }」に関係している?`
|
||||
case 'meme':
|
||||
return `元ネタ・ミームとして「${ label }」に関係しさう?`
|
||||
return `『${ label }』に関係しそう?`
|
||||
case 'character':
|
||||
return `「${ label }」といふキャラクターが関係してゐる?`
|
||||
return `「${ label }」というキャラクターが関係している?`
|
||||
case 'material':
|
||||
return `素材として「${ label }」に関係してゐる?`
|
||||
return `素材「${ label }」に関係している?`
|
||||
case 'nico':
|
||||
return `ニコニコに「${ label }」といふタグが付いてゐる?`
|
||||
return `ニコニコに「${ label }」といふタグがついている?`
|
||||
default:
|
||||
return `内容として「${ label }」に関係しさう?`
|
||||
return `「${ label }」が含まれる?`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,32 +916,30 @@ const matchingPostCountInIds = ({
|
||||
if (matched.size < ids.size)
|
||||
matched.forEach (postId => {
|
||||
if (ids.has (postId))
|
||||
count += 1
|
||||
++count
|
||||
})
|
||||
else
|
||||
ids.forEach (postId => {
|
||||
if (matched.has (postId))
|
||||
count += 1
|
||||
++count
|
||||
})
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
const matchingWeightInCandidates = ({
|
||||
candidates,
|
||||
posts,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
question,
|
||||
dynamicMatchIndex,
|
||||
}: {
|
||||
candidates: { post: Post; weight: number }[]
|
||||
posts: Post[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
question: GekanatorQuestion
|
||||
dynamicMatchIndex?: GekanatorMatchIndex
|
||||
}): number => {
|
||||
const matchingWeightInCandidates = (
|
||||
{ candidates,
|
||||
posts,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
question,
|
||||
dynamicMatchIndex }: { candidates: { post: Post; weight: number }[]
|
||||
posts: Post[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
question: GekanatorQuestion
|
||||
dynamicMatchIndex?: GekanatorMatchIndex },
|
||||
): number => {
|
||||
const matched = matchingPostIdsForQuestion ({
|
||||
posts,
|
||||
materialIndex,
|
||||
@@ -952,21 +951,19 @@ const matchingWeightInCandidates = ({
|
||||
sum + (matched.has (item.post.id) ? item.weight : 0), 0)
|
||||
}
|
||||
|
||||
const signatureForCandidateIds = ({
|
||||
candidateIds,
|
||||
posts,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
question,
|
||||
dynamicMatchIndex,
|
||||
}: {
|
||||
candidateIds: number[]
|
||||
posts: Post[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
question: GekanatorQuestion
|
||||
dynamicMatchIndex?: GekanatorMatchIndex
|
||||
}): string => {
|
||||
const signatureForCandidateIds = (
|
||||
{ candidateIds,
|
||||
posts,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
question,
|
||||
dynamicMatchIndex, }: { candidateIds: number[]
|
||||
posts: Post[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
question: GekanatorQuestion
|
||||
dynamicMatchIndex?: GekanatorMatchIndex },
|
||||
): string => {
|
||||
const matched = matchingPostIdsForQuestion ({
|
||||
posts,
|
||||
materialIndex,
|
||||
@@ -977,28 +974,24 @@ const signatureForCandidateIds = ({
|
||||
return candidateIds.map (postId => matched.has (postId) ? '1' : '0').join ('')
|
||||
}
|
||||
|
||||
const postIdsForHardAnswer = ({
|
||||
candidateIds,
|
||||
question,
|
||||
answer,
|
||||
posts,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
dynamicMatchIndex,
|
||||
}: {
|
||||
candidateIds: number[]
|
||||
question: GekanatorQuestion
|
||||
answer: GekanatorAnswerValue
|
||||
posts: Post[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
dynamicMatchIndex?: GekanatorMatchIndex
|
||||
}): number[] => {
|
||||
if (
|
||||
answer === 'unknown'
|
||||
const postIdsForHardAnswer = (
|
||||
{ candidateIds,
|
||||
question,
|
||||
answer,
|
||||
posts,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
dynamicMatchIndex }: { candidateIds: number[]
|
||||
question: GekanatorQuestion
|
||||
answer: GekanatorAnswerValue
|
||||
posts: Post[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
dynamicMatchIndex?: GekanatorMatchIndex },
|
||||
): number[] => {
|
||||
if (answer === 'unknown'
|
||||
|| answer === 'partial'
|
||||
|| answer === 'probably_no'
|
||||
)
|
||||
|| answer === 'probably_no')
|
||||
return candidateIds
|
||||
|
||||
if (answer === 'yes')
|
||||
@@ -1026,55 +1019,48 @@ const postIdsForHardAnswer = ({
|
||||
return candidateIds
|
||||
}
|
||||
|
||||
const buildIndexedQuestion = ({
|
||||
condition,
|
||||
const buildIndexedQuestion = (
|
||||
{ condition,
|
||||
text,
|
||||
kind,
|
||||
priorityWeight,
|
||||
materialIndex }: {
|
||||
condition: Exclude<GekanatorQuestionCondition, { type: 'post-similarity' }>
|
||||
text: string
|
||||
kind: GekanatorQuestionKind
|
||||
priorityWeight: number
|
||||
materialIndex: GekanatorQuestionMaterialIndex },
|
||||
): GekanatorQuestion => ({
|
||||
id: questionIdForCondition (condition),
|
||||
text,
|
||||
kind,
|
||||
condition,
|
||||
source: 'default',
|
||||
priorityWeight,
|
||||
materialIndex,
|
||||
}: {
|
||||
condition: Exclude<GekanatorQuestionCondition, { type: 'post-similarity' }>
|
||||
text: string
|
||||
kind: GekanatorQuestionKind
|
||||
priorityWeight: number
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
}): GekanatorQuestion => ({
|
||||
id: questionIdForCondition (condition),
|
||||
text,
|
||||
kind,
|
||||
condition,
|
||||
source: 'default',
|
||||
priorityWeight,
|
||||
test: post =>
|
||||
(matchingPostIdsForCondition ({
|
||||
condition,
|
||||
materialIndex }) ?? new Set<number> ()).has (post.id) })
|
||||
test: post =>
|
||||
(matchingPostIdsForCondition ({
|
||||
condition,
|
||||
materialIndex }) ?? new Set<number> ()).has (post.id) })
|
||||
|
||||
const rankedEntriesForCounts = <T extends string | number> ({
|
||||
counts,
|
||||
total,
|
||||
cap,
|
||||
}: {
|
||||
counts: Map<T, number>
|
||||
total: number
|
||||
cap: number
|
||||
}): [T, number][] =>
|
||||
[...counts.entries ()]
|
||||
.filter (([, count]) => count > 0 && count < total)
|
||||
.sort ((a, b) => Math.abs (total / 2 - a[1]) - Math.abs (total / 2 - b[1]))
|
||||
.slice (0, cap)
|
||||
const rankedEntriesForCounts = <T extends string | number> (
|
||||
{ counts, total, cap }: { counts: Map<T, number>
|
||||
total: number
|
||||
cap: number },
|
||||
): [T, number][] =>
|
||||
([...counts.entries ()]
|
||||
.filter (([, count]) => count > 0 && count < total)
|
||||
.sort ((a, b) => Math.abs (total / 2 - a[1]) - Math.abs (total / 2 - b[1]))
|
||||
.slice (0, cap))
|
||||
|
||||
const buildQuestionsForCandidateIds = ({
|
||||
candidateIds,
|
||||
materialIndex,
|
||||
performanceMode,
|
||||
acceptedQuestions,
|
||||
}: {
|
||||
candidateIds: number[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
performanceMode: GekanatorPerformanceMode
|
||||
acceptedQuestions: GekanatorQuestion[]
|
||||
}): GekanatorQuestion[] => {
|
||||
const buildQuestionsForCandidateIds = (
|
||||
{ candidateIds,
|
||||
materialIndex,
|
||||
performanceMode,
|
||||
acceptedQuestions }: { candidateIds: number[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
performanceMode: GekanatorPerformanceMode
|
||||
acceptedQuestions: GekanatorQuestion[] },
|
||||
): GekanatorQuestion[] => {
|
||||
const total = candidateIds.length
|
||||
if (total === 0)
|
||||
return acceptedQuestions
|
||||
@@ -1109,7 +1095,7 @@ const buildQuestionsForCandidateIds = ({
|
||||
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
|
||||
titleLengths.push (titleLength)
|
||||
if (materialIndex.titleAsciiPostIds.has (postId))
|
||||
asciiCount += 1
|
||||
++asciiCount
|
||||
})
|
||||
|
||||
const tagCap =
|
||||
@@ -1130,7 +1116,7 @@ const buildQuestionsForCandidateIds = ({
|
||||
.forEach (([host]) => {
|
||||
questions.push (buildIndexedQuestion ({
|
||||
condition: { type: 'source', host },
|
||||
text: `${ host } の投稿を思ひ浮かべてゐる?`,
|
||||
text: `${ host } の投稿を思い浮かべている?`,
|
||||
kind: 'source',
|
||||
priorityWeight: 1,
|
||||
materialIndex }))
|
||||
@@ -1180,7 +1166,7 @@ const buildQuestionsForCandidateIds = ({
|
||||
if (asciiCount > 0 && asciiCount < total)
|
||||
questions.push (buildIndexedQuestion ({
|
||||
condition: { type: 'title-has-ascii' },
|
||||
text: '題名に英数字が混じってゐる?',
|
||||
text: '題名に英数字が混じっている?',
|
||||
kind: 'title',
|
||||
priorityWeight: 1,
|
||||
materialIndex }))
|
||||
@@ -1959,9 +1945,9 @@ const winningRunTagText = (
|
||||
switch (category)
|
||||
{
|
||||
case 'nico':
|
||||
return `ニコニコに「${ name.replace (/^nico:/, '') }」タグが付いてゐる?`
|
||||
return `ニコニコに「${ name.replace (/^nico:/, '') }」タグがついている?`
|
||||
default:
|
||||
return `「${ name }」タグが付いてゐる?`
|
||||
return `「${ name }」タグがついている?`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2019,7 +2005,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
|
||||
? null
|
||||
: {
|
||||
id: questionIdForCondition ({ type: 'source', host }),
|
||||
text: `${ host } の投稿を思ひ浮かべてゐる?`,
|
||||
text: `${ host } の投稿を思い浮かべている?`,
|
||||
kind: 'source',
|
||||
condition: { type: 'source', host },
|
||||
source: 'default',
|
||||
@@ -2088,7 +2074,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
|
||||
})
|
||||
addQuestion ({
|
||||
id: questionIdForCondition ({ type: 'title-has-ascii' }),
|
||||
text: '題名に英数字が混じってゐる?',
|
||||
text: '題名に英数字が混じっている?',
|
||||
kind: 'title',
|
||||
condition: { type: 'title-has-ascii' },
|
||||
source: 'default',
|
||||
@@ -2107,7 +2093,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
|
||||
|
||||
targetPost.tags
|
||||
.filter (tag =>
|
||||
!(tag.category === 'meta')
|
||||
tag.category !== 'meta'
|
||||
&& !(tag.name.includes ('タグ希望'))
|
||||
&& !(tag.name.includes ('bot操作')))
|
||||
.slice (0, 20)
|
||||
@@ -2124,12 +2110,12 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
|
||||
test: post => post.tags.some (candidate =>
|
||||
candidate.category === tag.category
|
||||
&& candidate.name === tag.name
|
||||
&& !(candidate.category === 'meta')
|
||||
&& candidate.category !== 'meta'
|
||||
&& !(candidate.name.includes ('タグ希望'))
|
||||
&& !(candidate.name.includes ('bot操作'))) })
|
||||
})
|
||||
|
||||
;([
|
||||
void ([
|
||||
{
|
||||
answer: 'yes' as const,
|
||||
threshold: .9,
|
||||
@@ -2141,7 +2127,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
|
||||
{
|
||||
answer: 'no' as const,
|
||||
threshold: .25,
|
||||
text: '少し違ふ印象もある?' }]).forEach ((item, index) => {
|
||||
text: '少し違う印象もある?' }]).forEach ((item, index) => {
|
||||
addQuestion ({
|
||||
id: `winning-run:post-similarity:${ targetPost.id }:${ item.answer }:${ item.threshold }`,
|
||||
text: item.text,
|
||||
@@ -2342,8 +2328,10 @@ const chooseFallbackQuestion = ({
|
||||
|
||||
const shouldEnterGuessPhase = (
|
||||
reason: GuessReason | null,
|
||||
): reason is 'hard_max_questions' | 'winning_run_finished' =>
|
||||
reason === 'hard_max_questions' || reason === 'winning_run_finished'
|
||||
): reason is 'hard_max_questions' | 'winning_run_finished' | 'question_count_checkpoint' =>
|
||||
(reason === 'hard_max_questions'
|
||||
|| reason === 'winning_run_finished'
|
||||
|| reason === 'question_count_checkpoint')
|
||||
|
||||
|
||||
const isWinningRunActive = (
|
||||
@@ -2364,65 +2352,92 @@ const winningRunQuestionCount = (
|
||||
.length
|
||||
|
||||
|
||||
const nextQuestionPlanFor = ({
|
||||
posts,
|
||||
eligiblePosts,
|
||||
availablePosts,
|
||||
acceptedQuestions,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
lastGuessQuestionCount,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount,
|
||||
}: {
|
||||
posts: Post[]
|
||||
eligiblePosts: Post[]
|
||||
availablePosts: Post[]
|
||||
acceptedQuestions: GekanatorQuestion[]
|
||||
scores: Map<number, number>
|
||||
answers: GekanatorAnswerLog[]
|
||||
askedIds: Set<string>
|
||||
gameSeed: string
|
||||
recentFirstQuestionPenaltyById: Map<string, number>
|
||||
userPriorWeights: Map<number, number>
|
||||
performanceMode: GekanatorPerformanceMode
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
lastGuessQuestionCount: number
|
||||
winningRunTargetId: number | null
|
||||
winningRunStartAnswerCount: number | null
|
||||
}): {
|
||||
question: GekanatorQuestion | null
|
||||
guess: Post | null
|
||||
guessReason: GuessReason | null
|
||||
questionMode: QuestionMode
|
||||
winningRunTargetId: number | null
|
||||
winningRunStartAnswerCount: number | null
|
||||
} => {
|
||||
const nextQuestionPlanFor = (
|
||||
{ posts,
|
||||
eligiblePosts,
|
||||
availablePosts,
|
||||
acceptedQuestions,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
lastGuessQuestionCount,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount }: { posts: Post[]
|
||||
eligiblePosts: Post[]
|
||||
availablePosts: Post[]
|
||||
acceptedQuestions: GekanatorQuestion[]
|
||||
scores: Map<number, number>
|
||||
answers: GekanatorAnswerLog[]
|
||||
askedIds: Set<string>
|
||||
gameSeed: string
|
||||
recentFirstQuestionPenaltyById: Map<string, number>
|
||||
userPriorWeights: Map<number, number>
|
||||
performanceMode: GekanatorPerformanceMode
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
lastGuessQuestionCount: number
|
||||
winningRunTargetId: number | null
|
||||
winningRunStartAnswerCount: number | null },
|
||||
): { question: GekanatorQuestion | null
|
||||
guess: Post | null
|
||||
guessReason: GuessReason | null
|
||||
questionMode: QuestionMode
|
||||
winningRunTargetId: number | null
|
||||
winningRunStartAnswerCount: number | null } => {
|
||||
const guessablePosts =
|
||||
eligiblePosts.length > 0
|
||||
? eligiblePosts
|
||||
: availablePosts
|
||||
|
||||
const checkpointGuess =
|
||||
answers.length > 0
|
||||
&& answers.length - lastGuessQuestionCount >= minQuestionsBeforeCertainGuess
|
||||
|
||||
if (answers.length >= hardMaxQuestions)
|
||||
{
|
||||
return {
|
||||
question: null,
|
||||
guess: bestPost (guessablePosts, scores),
|
||||
guessReason: 'hard_max_questions',
|
||||
questionMode: null,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount }
|
||||
}
|
||||
|
||||
if (checkpointGuess)
|
||||
{
|
||||
return {
|
||||
question: null,
|
||||
guess: bestPost (guessablePosts, scores),
|
||||
guessReason: 'question_count_checkpoint',
|
||||
questionMode: null,
|
||||
winningRunTargetId,
|
||||
winningRunStartAnswerCount }
|
||||
}
|
||||
|
||||
const nextQuestionsSinceLastGuess = answers.length - lastGuessQuestionCount
|
||||
const nextWinningRunTargetId =
|
||||
eligiblePosts.length === 1
|
||||
? eligiblePosts[0]?.id ?? null
|
||||
: null
|
||||
? eligiblePosts[0]?.id ?? null
|
||||
: null
|
||||
const nextWinningRunStartAnswerCount =
|
||||
nextWinningRunTargetId === null
|
||||
? null
|
||||
: isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
||||
? null
|
||||
: ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
||||
&& winningRunTargetId === nextWinningRunTargetId
|
||||
&& winningRunStartAnswerCount !== null
|
||||
? winningRunStartAnswerCount
|
||||
: answers.length
|
||||
&& winningRunStartAnswerCount !== null)
|
||||
? winningRunStartAnswerCount
|
||||
: answers.length)
|
||||
const nextWinningRunTargetPost =
|
||||
nextWinningRunTargetId === null
|
||||
? null
|
||||
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
|
||||
? null
|
||||
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
|
||||
const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] =>
|
||||
buildQuestionsForCandidateIds ({
|
||||
candidateIds: scopePosts.map (post => post.id),
|
||||
@@ -2490,8 +2505,9 @@ const nextQuestionPlanFor = ({
|
||||
|
||||
const evaluationPosts =
|
||||
nextQuestionsSinceLastGuess >= minQuestionsBeforeCertainGuess
|
||||
? eligiblePosts
|
||||
: availablePosts
|
||||
? eligiblePosts
|
||||
: availablePosts
|
||||
|
||||
const evaluationQuestions = buildQuestionsForPosts (evaluationPosts)
|
||||
const normalQuestion = chooseQuestion ({
|
||||
posts: evaluationPosts,
|
||||
@@ -2505,6 +2521,7 @@ const nextQuestionPlanFor = ({
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex })
|
||||
|
||||
const fallbackQuestion = normalQuestion ?? chooseFallbackQuestion ({
|
||||
posts: evaluationPosts.length > 0 ? evaluationPosts : availablePosts,
|
||||
allPosts: posts,
|
||||
@@ -2514,19 +2531,17 @@ const nextQuestionPlanFor = ({
|
||||
scores,
|
||||
materialIndex,
|
||||
matchIndex })
|
||||
if (fallbackQuestion)
|
||||
return {
|
||||
question: fallbackQuestion,
|
||||
guess: null,
|
||||
guessReason: null,
|
||||
questionMode: 'normal',
|
||||
winningRunTargetId: nextWinningRunTargetId,
|
||||
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
|
||||
|
||||
const guessablePosts =
|
||||
eligiblePosts.length > 0
|
||||
? eligiblePosts
|
||||
: availablePosts
|
||||
if (fallbackQuestion)
|
||||
{
|
||||
return {
|
||||
question: fallbackQuestion,
|
||||
guess: null,
|
||||
guessReason: null,
|
||||
questionMode: 'normal',
|
||||
winningRunTargetId: nextWinningRunTargetId,
|
||||
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
|
||||
}
|
||||
|
||||
return {
|
||||
question: null,
|
||||
@@ -4184,8 +4199,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
|
||||
const dialogue =
|
||||
phase === 'learned' && resultWon
|
||||
? <>グカカカカwwwww <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>は何でもお見通し!</>
|
||||
: <>私は<ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>.質問から投稿を何でも当ててみせるよ</>
|
||||
? <>グカカカカwwwww <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>は何でもお見通し!</>
|
||||
: <>私は<ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>。質問から投稿を何でも当ててみせるよ。</>
|
||||
const introLoading = isLoading || acceptedQuestionsLoading
|
||||
const readyToStart =
|
||||
!(introLoading)
|
||||
@@ -4274,7 +4289,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
return (
|
||||
<MainArea className="relative overflow-hidden bg-yellow-50 dark:bg-red-975">
|
||||
<Helmet>
|
||||
<meta name="robots" content="noindex"/>
|
||||
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
@@ -4340,15 +4354,16 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<p className="text-lg font-bold">
|
||||
{dialogue}
|
||||
</p>
|
||||
{phase === 'intro' && (
|
||||
<p className="text-lg font-bold">
|
||||
{dialogue}
|
||||
</p>)}
|
||||
|
||||
{introLoading && (
|
||||
<p>
|
||||
{phase === 'intro'
|
||||
? '投稿を読み込んでゐます...'
|
||||
: '前回のグカネータ状態を復元してゐます...'}
|
||||
? '投稿を読み込んでいます……'
|
||||
: '前回のグカネータ状態を復元しています……'}
|
||||
</p>)}
|
||||
{(Boolean (error) || Boolean (acceptedQuestionsError))
|
||||
&& <p>グカネータの質問データを読み込めませんでした.</p>}
|
||||
@@ -4363,6 +4378,18 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
|
||||
{phase === 'intro' && readyToStart && restorePromptVisible && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<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={() => {
|
||||
reset ()
|
||||
setRestorePromptVisible (false)
|
||||
setPhase ('question')
|
||||
}}>
|
||||
最初からやり直す
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
|
||||
@@ -4370,14 +4397,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
onClick={continueStoredGame}>
|
||||
続きから
|
||||
</button>
|
||||
<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={reset}>
|
||||
最初からやり直す
|
||||
</button>
|
||||
</div>)}
|
||||
|
||||
{phase === 'intro' && readyToStart && !(restorePromptVisible) && (
|
||||
@@ -4389,7 +4408,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
setRestorePromptVisible (false)
|
||||
setPhase ('question')
|
||||
}}>
|
||||
はじめる
|
||||
始める
|
||||
</button>)}
|
||||
|
||||
{phase === 'question' && currentQuestion && (
|
||||
@@ -4490,7 +4509,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</div>)}
|
||||
{phase === 'guess' && displayedGuess && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xl font-bold">これを想像してゐたね?</p>
|
||||
<p className="text-xl font-bold">これを想像していたね?</p>
|
||||
{isAdmin && (
|
||||
<div className="rounded border border-yellow-100 px-3 py-2
|
||||
text-sm dark:border-red-900">
|
||||
@@ -4524,7 +4543,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
hover:bg-yellow-100 dark:border-red-700
|
||||
dark:hover:bg-red-900"
|
||||
onClick={rejectGuess}>
|
||||
違ふ
|
||||
違う
|
||||
</button>
|
||||
{history.length > 0 && (
|
||||
<button
|
||||
@@ -4577,7 +4596,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-neutral-500">ゲーム終了</p>
|
||||
<p className="text-xl font-bold">グカカカカwwwww</p>
|
||||
<p className="text-xl font-bold">グカカカカwwwww <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>は何でもお見通し!</p>
|
||||
</div>
|
||||
|
||||
{reviewGuessedPost && (
|
||||
@@ -4861,11 +4880,11 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-neutral-500">追加学習</p>
|
||||
<p className="text-xl font-bold">追加で 2 問まで答へてください。</p>
|
||||
<p className="text-xl font-bold">追加で 2 問まで答えてください。</p>
|
||||
</div>
|
||||
|
||||
{extraQuestionState === 'loading' && (
|
||||
<p>追加質問を読み込んでゐます...</p>)}
|
||||
<p>追加質問を読み込んでいます……</p>)}
|
||||
|
||||
{extraQuestionState === 'empty' && (
|
||||
<p>追加で学習できる質問はありませんでした。</p>)}
|
||||
|
||||
新しい課題から参照
ユーザをブロックする