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