diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index 2c64fd5..4dd7fbc 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -43,6 +43,18 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # public/se/ は規約上リポジトリに置けないので、checkout 直後は空。 + # ここで取らないと音源の無いアセットがそのまま公開される。 + # キャッシュが当たれば fetch-se は既存ファイルを skip し、相手のサイトは叩かない。 + - name: Cache sound effects + uses: actions/cache@v4 + with: + path: public/se + key: se-${{ hashFiles('scripts/fetch-se.ts') }} + + - name: Fetch sound effects + run: bun run se:fetch + - name: Build run: bun run build diff --git a/__tests__/api/validation.test.ts b/__tests__/api/validation.test.ts index 6a32329..77807d8 100644 --- a/__tests__/api/validation.test.ts +++ b/__tests__/api/validation.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'bun:test' import app from '@/server/index' +import { MAX_TOPIC_CHARS } from '@/shared/turns' /** * bun test は Workers ランタイムの外で動くので、バインディング(DO / KV / Hyperdrive)を @@ -120,21 +121,21 @@ describe('POST /api/sessions/:id/ask', () => { expect(res.status).toBe(400) }) - test('話題が501文字なら 400', async () => { + test('話題が上限を1文字でも超えれば 400', async () => { const res = await postJson(`/api/sessions/${SESSION_ID}/ask`, { sessionId: SESSION_ID, characterId: CHARACTER_ID, - topic: 'あ'.repeat(501), + topic: 'あ'.repeat(MAX_TOPIC_CHARS + 1), }) expect(res.status).toBe(400) }) - test('500文字ちょうどはバリデーションを通過する(境界の内側)', async () => { + test('上限ちょうどはバリデーションを通過する(境界の内側)', async () => { const res = await postJson(`/api/sessions/${SESSION_ID}/ask`, { sessionId: SESSION_ID, characterId: CHARACTER_ID, - topic: 'あ'.repeat(500), + topic: 'あ'.repeat(MAX_TOPIC_CHARS), }) // バインディングが無いのでこの先は進めない。ここで見たいのは @@ -151,6 +152,28 @@ describe('POST /api/sessions/:id/ask', () => { expect(res.status).toBe(400) }) + + test('調べられる場所も相手として受ける', async () => { + // 人物は uuid、遺体は victim、場所は作者が書いたローカルID。同じ口へ来る。 + const res = await postJson(`/api/sessions/${SESSION_ID}/ask`, { + sessionId: SESSION_ID, + characterId: 'choba', + topic: '帳面を見てみる', + }) + + expect(res.status).not.toBe(400) + }) + + test('相手の形をしていない文字列は 400', async () => { + // uuid でも victim でも、場所の ID の形でもないもの。 + const res = await postJson(`/api/sessions/${SESSION_ID}/ask`, { + sessionId: SESSION_ID, + characterId: '帳場', + topic: '帳面を見てみる', + }) + + expect(res.status).toBe(400) + }) }) describe('POST /api/sessions/:id/accuse', () => { diff --git a/__tests__/client/chat-log.test.ts b/__tests__/client/chat-log.test.ts index dcfb470..ad5e009 100644 --- a/__tests__/client/chat-log.test.ts +++ b/__tests__/client/chat-log.test.ts @@ -15,53 +15,82 @@ const textsOf = (item: ReturnType[number]): string[] => item.kind === 'topic' ? [item.text] : item.lines.map((line) => line.text) describe('groupTurns', () => { - test('空行で区切られた返答を行に割る', () => { - const items = groupTurns([ - turn('a1', 'assistant', 'えっ、Yさんですか?\n\nそうですね。いい人でしたよ。'), - ]) + test('返答を一文ずつに割る', () => { + const items = groupTurns( + [turn('a1', 'assistant', 'えっ、Yさんですか?\n\nそうですね。いい人でしたよ。')], + false, + ) expect(items).toHaveLength(1) - expect(textsOf(items[0])).toEqual(['えっ、Yさんですか?', 'そうですね。いい人でしたよ。']) + expect(textsOf(items[0])).toEqual(['えっ、Yさんですか?', 'そうですね。', 'いい人でしたよ。']) }) test('改行が続いても区切りは1つと数える', () => { - const items = groupTurns([turn('a1', 'assistant', '前半\n\n\n\n後半')]) + const items = groupTurns([turn('a1', 'assistant', '前半\n\n\n\n後半')], false) expect(textsOf(items[0])).toEqual(['前半', '後半']) }) test('割った行にはそれぞれ別の鍵が付く', () => { - const items = groupTurns([turn('a1', 'assistant', '一つ目\n\n二つ目')]) + const items = groupTurns([turn('a1', 'assistant', '一つ目\n\n二つ目')], false) const item = items[0] expect(item.kind === 'block' ? item.lines.map((line) => line.id) : []).toEqual(['a1:0', 'a1:1']) }) test('続けて喋った分は塊にまとまり、行が並ぶ', () => { - const items = groupTurns([ - turn('a1', 'assistant', '一つ目\n\n二つ目'), - turn('a2', 'assistant', '三つ目'), - ]) + const items = groupTurns( + [turn('a1', 'assistant', '一つ目\n\n二つ目'), turn('a2', 'assistant', '三つ目')], + false, + ) expect(items).toHaveLength(1) expect(textsOf(items[0])).toEqual(['一つ目', '二つ目', '三つ目']) }) test('話題を挟むと塊が分かれる', () => { - const items = groupTurns([ - turn('a1', 'assistant', '前の返答'), - turn('t1', 'topic', '次の話題'), - turn('u1', 'user', '次の質問'), - ]) + const items = groupTurns( + [ + turn('a1', 'assistant', '前の返答'), + turn('t1', 'topic', '次の話題'), + turn('u1', 'user', '次の質問'), + ], + false, + ) expect(items.map((item) => item.kind)).toEqual(['block', 'topic', 'block']) }) + test('流れている最中は、書きかけの一文を出さない', () => { + const items = groupTurns([turn('a1', 'assistant', 'そうですね。いい人でし')], true) + + expect(textsOf(items[0])).toEqual(['そうですね。']) + }) + + test('流れ終われば、句点で終わらない一文も出す', () => { + const items = groupTurns([turn('a1', 'assistant', 'そうですね。……雨でしたから')], false) + + expect(textsOf(items[0])).toEqual(['そうですね。', '……雨でしたから']) + }) + + test('末尾より前の返答は、流れている最中でも全部出す', () => { + const items = groupTurns( + [turn('a1', 'assistant', '前の返答は書き終わっている'), turn('a2', 'assistant', '書きかけ')], + true, + ) + + expect(textsOf(items[0])).toEqual(['前の返答は書き終わっている']) + }) + test('返答待ちの空のターンは置かない', () => { - expect(groupTurns([turn('a1', 'assistant', '')])).toEqual([]) + expect(groupTurns([turn('a1', 'assistant', '')], true)).toEqual([]) }) test('最初に届いたのが改行だけでも、まだ置かない', () => { - expect(groupTurns([turn('a1', 'assistant', '\n\n')])).toEqual([]) + expect(groupTurns([turn('a1', 'assistant', '\n\n')], true)).toEqual([]) + }) + + test('一文目が出来上がるまでは、名前も出さない', () => { + expect(groupTurns([turn('a1', 'assistant', 'そうですね')], true)).toEqual([]) }) }) diff --git a/__tests__/client/deadline.test.ts b/__tests__/client/deadline.test.ts new file mode 100644 index 0000000..1fb38ee --- /dev/null +++ b/__tests__/client/deadline.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from 'bun:test' +import { deadlineOf } from '@/client/lib/deadline' + +/** + * 被害者。死亡推定時刻は持っていない——あれは開示済みかどうかをサーバが判断して + * セッションの状態で運ぶもので、シナリオ詳細には最初から入ってこない + * (docs/design/deadline-window.md)。 + */ +const VICTIM = { + name: '水野英治', + introduction: '青雨堂店主', + foundAt: '19:10', + foundIn: '店の奥', + hasEstimatedDeathAt: true, + investigable: true, +} + +describe('deadlineOf', () => { + test('開示前は死亡推定が不明のまま', () => { + expect(deadlineOf(VICTIM, null)).toEqual({ + foundAt: '19:10', + label: '死亡推定', + death: { kind: 'unknown' }, + }) + }) + + /* + * 「まだ見つけていない」と「最初から無い」は別のこと。点線と ? は「ここに探すものがある」 + * という誘いなので、死亡推定時刻を持たない事件に出すと、無いものを探させることになる。 + */ + test('死亡推定時刻を持たない事件では、印そのものを出さない', () => { + expect(deadlineOf({ ...VICTIM, hasEstimatedDeathAt: false }, null)).toEqual({ + foundAt: '19:10', + label: '死亡推定', + death: undefined, + }) + }) + + test('開示されると死亡推定が確定して出る', () => { + expect(deadlineOf(VICTIM, '18:50')).toEqual({ + foundAt: '19:10', + label: '死亡推定', + death: { kind: 'fixed', at: '18:50' }, + }) + }) + + test('発見時刻を持たない事件では、遺体発見の線を出さない', () => { + expect(deadlineOf({ ...VICTIM, foundAt: null }, null)).toEqual({ + foundAt: undefined, + label: '死亡推定', + death: { kind: 'unknown' }, + }) + }) + + test('発見時刻が分かっていなくても、開示された死亡推定は出る', () => { + expect(deadlineOf({ ...VICTIM, foundAt: null }, '18:50')).toEqual({ + foundAt: undefined, + label: '死亡推定', + death: { kind: 'fixed', at: '18:50' }, + }) + }) + + test('被害者のいない事件では刻限そのものが無い', () => { + expect(deadlineOf(null, '18:50')).toBeUndefined() + }) +}) diff --git a/__tests__/client/restore.test.ts b/__tests__/client/restore.test.ts index d46f9aa..00d31fb 100644 --- a/__tests__/client/restore.test.ts +++ b/__tests__/client/restore.test.ts @@ -13,7 +13,15 @@ describe('restoreConversations', () => { history([ { characterId: 'a', - exchanges: [{ question: '昨夜どこに?', answer: '書斎です', askedAt: 100, topic: null }], + exchanges: [ + { + question: '昨夜どこに?', + answer: '書斎です', + askedAt: 100, + topic: null, + yielded: false, + }, + ], }, ]), ) @@ -32,15 +40,27 @@ describe('restoreConversations', () => { { characterId: 'a', exchanges: [ - { question: '昨夜どこに?', answer: '書斎です', askedAt: 100, topic: 'アリバイ' }, - { question: '何時まで?', answer: '日付が変わる頃まで', askedAt: 100, topic: null }, + { + question: '昨夜どこに?', + answer: '書斎です', + askedAt: 100, + topic: 'アリバイ', + yielded: false, + }, + { + question: '何時まで?', + answer: '日付が変わる頃まで', + askedAt: 100, + topic: null, + yielded: false, + }, ], }, ]), ) expect(result.a).toEqual([ - { id: '100:0', role: 'topic', text: 'アリバイ', askedAt: 100 }, + { id: '100:0', role: 'topic', text: 'アリバイ', askedAt: 100, notable: false }, { id: '100:1', role: 'user', text: '昨夜どこに?', askedAt: 100 }, { id: '100:2', role: 'assistant', text: '書斎です', askedAt: 100 }, { id: '100:3', role: 'user', text: '何時まで?', askedAt: 100 }, @@ -48,16 +68,43 @@ describe('restoreConversations', () => { ]) }) + test('何かを引き出した話題には印が立つ', () => { + const result = restoreConversations( + history([ + { + characterId: 'a', + exchanges: [ + { + question: '傘は?', + answer: '差していません', + askedAt: 100, + topic: '雨', + yielded: true, + }, + ], + }, + ]), + ) + + expect(result.a?.[0]).toEqual({ + id: '100:0', + role: 'topic', + text: '雨', + askedAt: 100, + notable: true, + }) + }) + test('質問と答えは同じ時刻を持つ(NPCをまたいで並べ直すため)', () => { const result = restoreConversations( history([ { characterId: 'a', - exchanges: [{ question: 'q1', answer: 'a1', askedAt: 300, topic: null }], + exchanges: [{ question: 'q1', answer: 'a1', askedAt: 300, topic: null, yielded: false }], }, { characterId: 'b', - exchanges: [{ question: 'q2', answer: 'a2', askedAt: 200, topic: null }], + exchanges: [{ question: 'q2', answer: 'a2', askedAt: 200, topic: null, yielded: false }], }, ]), ) @@ -71,13 +118,15 @@ describe('restoreConversations', () => { history([ { characterId: 'a', - exchanges: [{ question: '聞きかけ', answer: '', askedAt: 1, topic: '話題' }], + exchanges: [ + { question: '聞きかけ', answer: '', askedAt: 1, topic: '話題', yielded: false }, + ], }, ]), ) expect(result.a).toEqual([ - { id: '1:0', role: 'topic', text: '話題', askedAt: 1 }, + { id: '1:0', role: 'topic', text: '話題', askedAt: 1, notable: false }, { id: '1:1', role: 'user', text: '聞きかけ', askedAt: 1 }, ]) }) @@ -85,7 +134,10 @@ describe('restoreConversations', () => { test('一度も話していないNPCはキーごと作らない', () => { const result = restoreConversations( history([ - { characterId: 'a', exchanges: [{ question: 'q', answer: 'a', askedAt: 1, topic: null }] }, + { + characterId: 'a', + exchanges: [{ question: 'q', answer: 'a', askedAt: 1, topic: null, yielded: false }], + }, { characterId: 'b', exchanges: [] }, ]), ) diff --git a/__tests__/db/author.test.ts b/__tests__/db/author.test.ts index 598aa18..e350696 100644 --- a/__tests__/db/author.test.ts +++ b/__tests__/db/author.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test } from 'bun:test' -import { type AuthorGenerateRequest, describeIssues, runAuthor } from '~/db/author' +import { + type AuthorGenerateRequest, + authoringWarnings, + describeIssues, + runAuthor, +} from '~/db/author' +import { validateScenario } from '~/db/compile-scenario' import { loadScenarioYaml } from '~/db/scenario-file' /* @@ -8,6 +14,32 @@ import { loadScenarioYaml } from '~/db/scenario-file' */ const valid = await loadScenarioYaml('tsukimisou') +/* + 警告の出る定義は、月見荘から record を剥がして作る。実物が警告を持つのを + 当てにしない——持っていない状態が正しいので、当てにすると穴が埋まった日に + このテストが落ちる(実際に一度そうなった)。 +*/ +const withWarning = (() => { + const definition = structuredClone(valid) + + if ( + definition === null || + typeof definition !== 'object' || + !('timeline' in definition) || + !Array.isArray(definition.timeline) + ) { + throw new Error('月見荘の定義を読めませんでした') + } + + const event = definition.timeline.find(({ id }) => id === 'ryoko-drinks') + + if (event === undefined) throw new Error('ryoko-drinks が見当たりません') + + event.record = undefined + + return definition +})() + /** 参照を1本壊した定義。構造は合っているが superRefine が落とす。 */ const brokenReference = () => { const definition = structuredClone(valid) @@ -112,6 +144,66 @@ describe('runAuthor', () => { }) }) +describe('runAuthor: 警告', () => { + test('警告だけでも差し戻し、直れば採用する', async () => { + const { generate, seen } = scriptedGenerate([withWarning, valid]) + const result = await runAuthor({ premise: '題材', generate, maxAttempts: 3 }) + + expect(result.ok).toBe(true) + expect(seen).toHaveLength(2) + expect(seen[1]?.previous?.issues.join('\n')).toContain('record') + + expect(result.attempts).toHaveLength(1) + if (!result.ok) return + expect(result.warnings).toEqual([]) + }) + + /* + 警告は検証の失敗ではないので、最後の一回で捨ててはいけない。 + 捨てると、通る定義が手元にあるのに手ぶらで終わる。 + */ + test('最後の一回まで残った警告は、付けたまま採用する', async () => { + const { generate, seen } = scriptedGenerate([withWarning]) + const result = await runAuthor({ premise: '題材', generate, maxAttempts: 1 }) + + expect(result.ok).toBe(true) + expect(seen).toHaveLength(1) + if (!result.ok) return + expect(result.warnings).toHaveLength(1) + }) +}) + +describe('authoringWarnings', () => { + const definitionOf = (input: unknown) => { + const validated = validateScenario(input) + + if (!validated.ok) throw new Error(`検証を通りませんでした:\n${validated.issues.join('\n')}`) + + return validated.definition + } + + test('物証で裏付けられた出来事に record が無ければ、その出来事を名指しする', () => { + const warnings = authoringWarnings(definitionOf(withWarning)) + + expect(warnings).toHaveLength(1) + expect(warnings[0]).toContain('ryoko-drinks') + }) + + test('record が埋まっていれば何も言わない', () => { + expect(authoringWarnings(definitionOf(valid))).toEqual([]) + }) + + test('崩せる嘘が時刻表に繋がっていなければ、印が立たないと言う', () => { + const definition = definitionOf(valid) + const severed = { + ...definition, + evidences: definition.evidences.map((evidence) => ({ ...evidence, contradicts: [] })), + } + + expect(authoringWarnings(severed).join('\n')).toContain('食い違い') + }) +}) + describe('describeIssues', () => { test('件数を先に出し、指摘を箇条書きで並べる', () => { expect(describeIssues(['a が壊れています', 'b が足りません'])).toBe( diff --git a/__tests__/db/compile-scenario.test.ts b/__tests__/db/compile-scenario.test.ts index e3fdd3c..3d3cf56 100644 --- a/__tests__/db/compile-scenario.test.ts +++ b/__tests__/db/compile-scenario.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test' import { compileScenario } from '~/db/compile-scenario' import { parseFloorPlan } from '~/db/floor-plan' import { TSUKIMISOU_PLAN } from '~/db/floor-plans/tsukimisou' -import type { ScenarioDefinitionInput } from '~/db/scenario-definition' +import { type ScenarioDefinitionInput, VICTIM_ID } from '~/db/scenario-definition' import { loadScenarioYaml } from '~/db/scenario-file' const TSUKIMISOU_SCENARIO = await loadScenarioYaml('tsukimisou') @@ -149,9 +149,9 @@ describe('compileScenario: 月見荘のコンパイル', () => { test('公開状態と件数', () => { expect(compiled.scenario.isPublished).toBe(true) - expect(compiled.scenario.title).toBe('月見荘、十七回忌の夜') + expect(compiled.scenario.title).toBe('十七回忌の客') expect(compiled.characters).toHaveLength(3) - expect(compiled.evidences).toHaveLength(6) + expect(compiled.evidences).toHaveLength(9) expect(compiled.revelations).toHaveLength(2) }) @@ -221,6 +221,11 @@ describe('compileScenario: プロンプトを壊さない不変条件', () => { if (source.type === 'character') { expect(characterIds.has(source.id)).toBe(true) expect(roomIds.has(source.id)).toBe(false) + } else if (source.type === 'victim') { + // 被害者は一人しか居ないので採番しない。決め打ちのIDのまま焼かれる。 + expect(source.id).toBe(VICTIM_ID) + expect(characterIds.has(source.id)).toBe(false) + expect(roomIds.has(source.id)).toBe(false) } else { expect(roomIds.has(source.id)).toBe(true) expect(characterIds.has(source.id)).toBe(false) @@ -263,7 +268,7 @@ describe('compileScenario: プロンプトを壊さない不変条件', () => { const timeline = compiled.truth.timeline expect(Array.isArray(timeline)).toBe(true) - expect(timeline).toHaveLength(9) + expect(timeline).toHaveLength(10) expect(timeline).toEqual( expect.arrayContaining([ { time: '19:00', event: '夕食会が始まる。涼子・深川・美月・桐生の4人が同席。' }, @@ -501,3 +506,66 @@ describe('compileScenario: 被害者', () => { expect(result.scenario.timeEnd).not.toBeNull() }) }) + +describe('被害者の所見', () => { + test('解禁の前提は uuid へ採番される', () => { + // ローカルIDのまま焼くと、DOが持つ uuid と突き合わない=前提が永久に満たされない。 + const evidenceIds = new Set(compiled.evidences.map((evidence) => evidence.id)) + const required = compiled.truth.victimFindings.flatMap((finding) => finding.requires.evidences) + + expect(required.length).toBeGreaterThan(0) + + for (const id of required) { + expect(evidenceIds.has(id)).toBe(true) + } + }) + + test('遺体を調べられる事件として焼かれる', () => { + expect(compiled.scenario.victimInvestigable).toBe(true) + expect(compiled.scenario.victimFoundAt).toBe('20:30') + expect(compiled.truth.victimCauseOfDeath).not.toBeNull() + }) +}) + +describe('死亡推定時刻を明かす印', () => { + test('印を立てた証拠だけが true で焼かれる', () => { + const marked = compiled.evidences.filter((evidence) => evidence.revealsDeathTime) + + // 検死の一件と、医師の見立ての一件。どちらの道からでも刻限へ辿り着ける。 + expect(marked.map((evidence) => evidence.label)).toEqual([ + '遺体に残る中毒の徴候と、その進み具合', + '桐生が医師として述べた死亡推定時刻', + ]) + }) + + test('印の無い証拠は false。時刻は公開側の列にだけ載る', () => { + const unmarked = compiled.evidences.filter((evidence) => !evidence.revealsDeathTime) + + expect(unmarked.length).toBeGreaterThan(0) + // 時刻そのものは印と別の場所。サーバが両方を突き合わせて初めて盤面へ出る。 + expect(compiled.scenario.victimEstimatedDeathAt).toBe('20:15') + }) + + test('死亡推定時刻を持たない事件では印を立てられない', () => { + const definition = makeMinimal() + const result = compileScenario( + { + ...definition, + victim: { name: '水野英治', introduction: '青雨堂店主' }, + evidences: [ + { + id: 'coroner-note', + label: '検死の覚え書き', + reveal: { condition: '遺体を調べたら開示する。' }, + revealsDeathTime: true, + }, + ], + }, + { isPublished: true, newId: sequentialIds() }, + ) + + expect(result.ok).toBe(false) + // 開けるべき時刻がどこにも無いまま印だけが立つと、掴んでも盤面が変わらない。 + expect(result.ok ? [] : result.issues.join('\n')).toContain('revealsDeathTime') + }) +}) diff --git a/__tests__/db/scenario-current-authoring.test.ts b/__tests__/db/scenario-current-authoring.test.ts new file mode 100644 index 0000000..1d25a59 --- /dev/null +++ b/__tests__/db/scenario-current-authoring.test.ts @@ -0,0 +1,434 @@ +import { readdir } from 'node:fs/promises' +import path from 'node:path' +import YAML from 'yaml' +import { ScenarioDefinitionSchema } from '../../db/scenario-definition' + +const SCENARIO_DIR = path.resolve(import.meta.dir, '../../db/scenarios') + +const scenarioFiles = async (): Promise => + (await readdir(SCENARIO_DIR)).filter((name) => name.endsWith('.yaml')).sort() + +const scenarios = async () => { + const loaded = [] + + for (const file of await scenarioFiles()) { + const source = await Bun.file(path.join(SCENARIO_DIR, file)).text() + const parsed = ScenarioDefinitionSchema.safeParse(YAML.parse(source)) + if (!parsed.success) throw new Error(`${file}: invalid scenario schema`) + loaded.push({ file, scenario: parsed.data }) + } + + return loaded +} + +const LEGACY_TITLES = new Set([ + '白樺峰に雪崩が落ちた夜', + '山上の修道院から誰も帰れない', + '四人だけの研修ロッジ', + '救助隊が来るまで', + '四十七年目の白樺館', + '海底居住区アビス3', + '霧の中を進む「しおかぜ」', + '祭りが暗くなった八分間', + '高潮警報、文書館閉鎖', + '河川氾濫、旧南央裁判所', + '内覧会は終わっていた', + '2312年、世代船アステリア', + '開園前、標本庫にて', + '締切後の青燈社', + '山道が崩れた時計博物館', + '崖の上から帰れない', + '終電が八分遅れた夜', + '火星、エリュシオン観測基地', + '午前零時十二分、第二収録ブース', + '白夜第六観測基地', + '封鎖された白嶺診療所', + '青雨堂、閉店後の商談', + '1928年、上海河岸', + '雪は白庭彫刻館を閉ざした', + '北岳観測所、吹雪の午後十時', + '朝七時、高原農園', + '白環館、雪の作品保存庫', + 'ノース・レイクの深夜録音', + '白燕座、雪の終演後', + '閉館後の海浜水族館', + '増水する山中発電所', + '船の来ない青凪荘', + '補給船の来ない夕凪灯台', + '梢庵から出られない', + '星見ヶ丘、ロープウェイ停止', + '退避航行中の「みなも」', + '道路封鎖、山中研究会館', + '落雷停止、霧岳山頂駅', + '十七回忌、月見荘にて', + '冠水する湾岸データセンター', + '台風圏の洋上風力基地', + '1796年、検疫島ラッザレット', + '1863年、地下鉄工事区画', + '雪籠りの白樺峰', + '雪嶺修道院', + '白雪研修館', + '地底研究所', + '白樺館、四十七年', + '深海区画アビス3', + '霧航船しおかぜ', + '宵祭り', + '高潮の文書館', + '水際の旧南央裁判所', + '青環美術館夜想', + '世代船アステリア', + '緑苑植物園', + '青燈社深夜録', + '山麓時計博物館', + '崖上ホテル', + '夕凪駅、終夜', + 'エリュシオン砂嵐', + 'レイライン午前零時', + '白夜第六基地', + '白嶺診療所', + '青雨堂雨譚', + '上海河岸倉庫', + '白庭彫刻館', + '北岳観測所', + '雪籠りの高原農園', + '雪の白環館', + '録音所ノース・レイク', + '雪夜の白燕座', + '海浜水族館、閉館後', + '豪雨の発電所', + '孤島の青凪荘', + '夕凪灯台', + '梢庵夜話', + '星見ヶ丘天象館', + '調査船みなも', + '山中研究会館', + '霧岳山頂駅', + '月見荘十七回忌', + '湾岸データセンター', + '洋上風力基地', + 'ラッザレットの夕映え', + '霧都地下工事録', +]) + +const TIME_DECEPTION_PATTERN = /(死亡時刻|生存時刻|事件時刻|死亡後)/ + +// 43本を真相・時系列・嘘・証拠まで読み直したうえで、 +// 「死後の出来事を生存証明に見せる/時計の基準をずらす」こと自体が解法の軸になる事件。 +// 文言だけの正規表現では取りこぼすので、レビュー結果を明示しておく。 +const REVIEWED_DEATH_TIME_CASES = new Set([ + 'avalanche-monastery-bell-window.yaml', + 'coldcase-lodge-borrowed-memory.yaml', + 'generation-ship-staggered-dawn.yaml', + 'ink-stained-contract.yaml', + 'landslide-clock-museum-eleven-minutes.yaml', + 'landslide-hotel-frosted-silhouette.yaml', + 'midnight-radio-rerun.yaml', + 'quarantine-clinic-borrowed-badge.yaml', + 'shanghai-warehouse-carbon-copy.yaml', + 'snowbound-farm-morning-chores.yaml', + 'storm-island-scheduled-mail.yaml', + 'storm-mountain-inn-echoed-cane.yaml', + 'storm-planetarium-reflected-witness.yaml', + 'victorian-underground-last-telegram.yaml', +]) + +// 場所そのものの設備・構造を確かめる行為が、人物への聞き込みとは別の推理になる事件。 +// 「places が新機能だから」ではなく、全件レビューで一手を使う価値があると判断したものだけ。 +const REVIEWED_PLACE_CASES = new Set([ + 'avalanche-monastery-bell-window.yaml', + 'cave-lab-locator-cart.yaml', + 'coldcase-lodge-borrowed-memory.yaml', + 'flood-archive-self-locking-vault.yaml', + 'gallery-blue-frame.yaml', + 'landslide-clock-museum-eleven-minutes.yaml', + 'midnight-radio-rerun.yaml', + 'quarantine-clinic-borrowed-badge.yaml', + 'snowbound-gallery-wrong-crime-scene.yaml', + 'snowbound-studio-roomtone-loop.yaml', + 'storm-hydropower-rising-walkway.yaml', + 'storm-mountain-inn-echoed-cane.yaml', + 'storm-planetarium-reflected-witness.yaml', + 'thunder-cablecar-occupied-cabin.yaml', + 'victorian-underground-last-telegram.yaml', +]) + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const recordsOf = (value: unknown): Record[] => + Array.isArray(value) ? value.filter(isRecord) : [] + +describe('scenario current authoring guide', () => { + test('殺人事件の victim は遺体・現場を調べられる情報を持つ', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + const victim = scenario.victim + if (victim === undefined) { + violations.push(`${file}: victim missing`) + continue + } + + if (victim.foundAt === undefined) violations.push(`${file}: victim.foundAt`) + if (victim.foundIn === undefined) violations.push(`${file}: victim.foundIn`) + if (file !== 'coldcase-lodge-borrowed-memory.yaml') { + if (victim.causeOfDeath === undefined) violations.push(`${file}: victim.causeOfDeath`) + if (victim.findings.length < 2) violations.push(`${file}: victim.findings < 2`) + } + } + + expect(violations).toEqual([]) + }) + + test('過去の未解決事件は現在の遺体ではなく旧捜査資料を調べる', async () => { + const item = (await scenarios()).find( + ({ file }) => file === 'coldcase-lodge-borrowed-memory.yaml', + ) + expect(item).toBeDefined() + if (item === undefined) return + + expect(item.scenario.victim?.causeOfDeath).toBeUndefined() + expect(item.scenario.victim?.findings).toEqual([]) + expect(item.scenario.places.length).toBeGreaterThan(0) + }) + + test('動機へ繋がる手掛かりが最低1つ victim source から取れる', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + const motiveFacts = new Set( + scenario.facts.filter((fact) => fact.kind === 'motive').map((fact) => fact.id), + ) + const evidenceHasMotiveClue = scenario.evidences.some( + (evidence) => + evidence.sources.some((source) => source.type === 'victim') && + evidence.supports.some((factId) => motiveFacts.has(factId)), + ) + const revelationHasMotiveClue = scenario.revelations.some( + (revelation) => + revelation.category === 'motive' && + revelation.sources.some((source) => source.type === 'victim'), + ) + + const locationHasMotiveClue = scenario.evidences.some( + (evidence) => + evidence.sources.some((source) => source.type === 'location') && + evidence.supports.some((factId) => motiveFacts.has(factId)), + ) + + if ( + !evidenceHasMotiveClue && + !revelationHasMotiveClue && + !(file === 'coldcase-lodge-borrowed-memory.yaml' && locationHasMotiveClue) + ) { + violations.push(`${file}: no stable motive clue`) + } + } + + expect(violations).toEqual([]) + }) + + test('timeline はアリバイ表向けの短い在所を持つ', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + for (const event of scenario.timeline) { + if (event.location === undefined) { + violations.push(`${file}: timeline:${event.id}: location missing`) + } else if ([...event.location].length > 8) { + violations.push(`${file}: timeline:${event.id}: location too long`) + } + } + } + + expect(violations).toEqual([]) + }) + + test('現行フォーマットから外れた旧フィールドを authoring YAML に残さない', async () => { + const violations: string[] = [] + + for (const file of await scenarioFiles()) { + const source = await Bun.file(path.join(SCENARIO_DIR, file)).text() + const parsed: unknown = YAML.parse(source) + if (!isRecord(parsed)) { + violations.push(`${file}: invalid YAML root`) + continue + } + + const meta = isRecord(parsed.meta) ? parsed.meta : undefined + if (meta?.tags !== undefined) violations.push(`${file}: meta.tags`) + + recordsOf(parsed.facts).forEach((fact, index) => { + if (fact.secret !== undefined) violations.push(`${file}: facts[${index}].secret`) + }) + + recordsOf(parsed.characters).forEach((character, characterIndex) => { + if (character.role !== undefined) { + violations.push(`${file}: characters[${characterIndex}].role`) + } + recordsOf(character.memories).forEach((memory, memoryIndex) => { + if (memory.about !== undefined) { + violations.push(`${file}: characters[${characterIndex}].memories[${memoryIndex}].about`) + } + }) + }) + + recordsOf(parsed.evidences).forEach((evidence, index) => { + const reveal = isRecord(evidence.reveal) ? evidence.reveal : undefined + if (reveal?.mode !== undefined) { + violations.push(`${file}: evidences[${index}].reveal.mode`) + } + }) + + const solution = isRecord(parsed.solution) ? parsed.solution : undefined + if (solution?.requiredFacts !== undefined) violations.push(`${file}: solution.requiredFacts`) + if (parsed.quality !== undefined) violations.push(`${file}: quality`) + } + + expect(violations).toEqual([]) + }) + + test('死亡・生存時刻の偽装を核にする事件は死亡推定時刻を持つ', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + const reviewedAsTimeCase = REVIEWED_DEATH_TIME_CASES.has(file) + if ( + (reviewedAsTimeCase || TIME_DECEPTION_PATTERN.test(scenario.solution.method)) && + scenario.victim?.estimatedDeathAt === undefined + ) { + violations.push(`${file}: victim.estimatedDeathAt`) + } + } + + expect(violations).toEqual([]) + }) + + test('死亡推定時刻を置いた事件には、それを手掛かりから開ける経路がある', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + if (scenario.victim?.estimatedDeathAt === undefined) continue + + const marked = scenario.evidences.filter((evidence) => evidence.revealsDeathTime) + if (marked.length === 0) { + violations.push(`${file}: no revealsDeathTime evidence`) + continue + } + + const routes = new Set( + marked.flatMap((evidence) => + evidence.sources.map((source) => `${source.type}:${source.id}`), + ), + ) + if (routes.size < 2) { + violations.push(`${file}: death-time routes < 2`) + } + } + + expect(violations).toEqual([]) + }) + + test('精読で場所調査が有効と判断した事件には、空振りしない調査場所を置く', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + if (REVIEWED_PLACE_CASES.has(file) && scenario.places.length === 0) { + violations.push(`${file}: places missing`) + } + + for (const place of scenario.places) { + const hasSource = + scenario.evidences.some((evidence) => + evidence.sources.some((source) => source.type === 'location' && source.id === place.id), + ) || + scenario.revelations.some((revelation) => + revelation.sources.some( + (source) => source.type === 'location' && source.id === place.id, + ), + ) + if (!hasSource) violations.push(`${file}: place:${place.id}: no source route`) + } + } + + expect(violations).toEqual([]) + }) + + test('物証で時刻が裏付けられる出来事は record を持ち、record は物証か第三者観察に基づく', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + const kinds = new Map(scenario.facts.map((fact) => [fact.id, fact.kind])) + for (const event of scenario.timeline) { + const hasPhysical = event.facts.some((factId) => kinds.get(factId) === 'physical') + const backed = event.facts.some((factId) => { + const kind = kinds.get(factId) + return kind === 'physical' || kind === 'observation' + }) + + if (hasPhysical && event.record === undefined) { + violations.push(`${file}: timeline:${event.id}: record missing`) + } + if (event.record !== undefined && !backed) { + violations.push(`${file}: timeline:${event.id}: unsupported record`) + } + } + } + + expect(violations).toEqual([]) + }) + + test('離れた場所からの目撃・受信を同じ participants に混ぜない', async () => { + const byFile = new Map((await scenarios()).map(({ file, scenario }) => [file, scenario])) + const event = (file: string, id: string) => + byFile.get(file)?.timeline.find((item) => item.id === id) + + expect(event('rainy-bookstore-receipt.yaml', 'kuroda-sighting')).toMatchObject({ + participants: ['makino'], + witnesses: ['kuroda'], + }) + expect(event('rainy-bookstore-receipt.yaml', 'makino-departs')).toMatchObject({ + participants: ['makino'], + witnesses: ['sena'], + }) + expect(event('flood-archive-self-locking-vault.yaml', 'yagami-enters')).toMatchObject({ + participants: ['yagami'], + witnesses: ['kuga'], + }) + expect(event('storm-mountain-inn-echoed-cane.yaml', 'tapping-staged')).toMatchObject({ + participants: ['akiwa'], + witnesses: ['morisaki'], + }) + expect(event('victorian-underground-last-telegram.yaml', 'false-telegram')).toMatchObject({ + participants: ['bell'], + location: '第2立坑', + }) + expect(event('victorian-underground-last-telegram.yaml', 'telegram-received')).toMatchObject({ + participants: ['clara'], + location: '第1立坑', + }) + expect( + event('snowbound-farm-morning-chores.yaml', 'shortage-discovered')?.participants, + ).toEqual([]) + expect(event('blizzard-lodge-seat-score-alibi.yaml', 'fraud-discovered')?.participants).toEqual( + [], + ) + }) + + test('タイトルは今回の全面改稿前の題名を残さない', async () => { + const violations: string[] = [] + const seen = new Set() + let negativeTitleCount = 0 + + for (const { file, scenario } of await scenarios()) { + const title = scenario.meta.title + if (LEGACY_TITLES.has(title)) violations.push(`${file}: legacy title`) + if (seen.has(title)) violations.push(`${file}: duplicate title`) + if (/ない|来ない|帰れない/.test(title)) negativeTitleCount += 1 + seen.add(title) + } + + if (negativeTitleCount > 5) violations.push(`negative titles: ${negativeTitleCount}`) + expect(violations).toEqual([]) + }) +}) diff --git a/__tests__/db/scenario-definition.test.ts b/__tests__/db/scenario-definition.test.ts index 27aa427..111824b 100644 --- a/__tests__/db/scenario-definition.test.ts +++ b/__tests__/db/scenario-definition.test.ts @@ -8,13 +8,13 @@ import { scenarioLieSchema, scenarioMemorySchema, scenarioMetaSchema, - scenarioQualitySchema, scenarioRelationshipSchema, scenarioRevelationSchema, scenarioRevelationSourceSchema, scenarioSecretSchema, scenarioSolutionSchema, scenarioTimelineEventSchema, + VICTIM_ID, } from '~/db/scenario-definition' const validScenario: ScenarioDefinition = { @@ -26,7 +26,6 @@ const validScenario: ScenarioDefinition = { category: '学園ミステリー', difficulty: 2, estimatedMinutes: 10, - tags: ['theft', 'school'], }, briefing: '放課後18時30分、美術室からコンクール提出予定の作品がなくなっていることが分かった。', floorPlan: null, @@ -35,19 +34,16 @@ const validScenario: ScenarioDefinition = { id: 'painting-present-at-1800', statement: '18:00 の時点では作品は美術室にあった', kind: 'observation', - secret: false, }, { id: 'b-seen-at-1810', statement: '18:10 に B は美術室前の廊下にいた', kind: 'observation', - secret: false, }, { id: 'b-took-painting', statement: 'B が作品を持ち出した', kind: 'truth', - secret: true, }, ], timeline: [ @@ -70,7 +66,6 @@ const validScenario: ScenarioDefinition = { { id: 'a', name: '美術部員 A', - role: 'witness', publicIntroduction: '美術部員。', personality: '真面目で慎重。', goals: ['知っていることには正直に答える'], @@ -80,7 +75,6 @@ const validScenario: ScenarioDefinition = { memories: [ { id: 'saw-b', - about: 'b-seen-at-1810', detail: '18:10ごろ、Bと廊下ですれ違った。', }, ], @@ -95,7 +89,6 @@ const validScenario: ScenarioDefinition = { { id: 'b', name: '美術部員 B', - role: 'suspect', publicIntroduction: '美術部員。', personality: '負けず嫌い。追及されると防御的になる。', goals: ['自分が作品を持ち出したことを隠す'], @@ -117,7 +110,6 @@ const validScenario: ScenarioDefinition = { memories: [ { id: 'took-painting', - about: 'b-took-painting', detail: '作品を鞄に入れて美術室から持ち出した。', }, ], @@ -137,7 +129,6 @@ const validScenario: ScenarioDefinition = { label: '廊下の入退室記録', description: '18:08から18:12の間にBのカードが美術室前で記録されている。', reveal: { - mode: 'conversation', condition: '入退室記録や廊下の人の動きについて具体的に尋ねる', }, sources: [{ type: 'character', id: 'b' }], @@ -150,14 +141,8 @@ const validScenario: ScenarioDefinition = { summary: 'Bが18:10ごろ美術室から作品を持ち出した。', method: '施錠前の美術室に入り、額縁ごと持ち去った。', motive: 'competition', - requiredFacts: ['b-seen-at-1810', 'b-took-painting'], secretKeywords: ['Bが作品を持ち出した'], }, - quality: { - expectedQuestionCount: { min: 4, max: 12 }, - requiredEvidence: { min: 1 }, - redHerrings: [], - }, } const requiredAt = (items: T[], index: number): T => { @@ -273,17 +258,6 @@ describe('ScenarioDefinitionSchema: 正常系とトップレベル', () => { expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(true) }) - - test('quality を省略すると既定値が入る', () => { - const scenario = makeScenario() - Reflect.deleteProperty(scenario, 'quality') - - const result = ScenarioDefinitionSchema.safeParse(scenario) - - expect(result.success).toBe(true) - if (!result.success) return - expect(result.data.quality.redHerrings).toEqual([]) - }) }) describe('scenarioMetaSchema', () => { @@ -295,12 +269,6 @@ describe('scenarioMetaSchema', () => { estimatedMinutes: 10, } - test('tags を省略すると空配列になる', () => { - const result = scenarioMetaSchema.parse(minimalMeta) - - expect(result.tags).toEqual([]) - }) - test('文字列の前後空白を除去する', () => { const result = scenarioMetaSchema.parse({ ...minimalMeta, title: ' 題名 ' }) @@ -370,31 +338,9 @@ describe('scenarioMetaSchema', () => { expect(scenarioMetaSchema.safeParse({ ...minimalMeta, estimatedMinutes }).success).toBe(false) }) } - - test('tag は50文字を受理する', () => { - expect(scenarioMetaSchema.safeParse({ ...minimalMeta, tags: ['x'.repeat(50)] }).success).toBe( - true, - ) - }) - - test('tag は51文字を拒否する', () => { - expect(scenarioMetaSchema.safeParse({ ...minimalMeta, tags: ['x'.repeat(51)] }).success).toBe( - false, - ) - }) - - test('空白だけの tag を拒否する', () => { - expect(scenarioMetaSchema.safeParse({ ...minimalMeta, tags: [' '] }).success).toBe(false) - }) }) describe('scenarioFactSchema', () => { - test('secret を省略すると false になる', () => { - const result = scenarioFactSchema.parse({ id: 'fact', statement: '事実' }) - - expect(result.secret).toBe(false) - }) - test('fact ID は100文字を受理する', () => { expect(scenarioFactSchema.safeParse({ id: 'x'.repeat(100), statement: '事実' }).success).toBe( true, @@ -591,24 +537,6 @@ describe('character の構造', () => { ) }) - test('role は50文字を受理する', () => { - expect( - scenarioCharacterSchema.safeParse({ ...minimalCharacter, role: 'x'.repeat(50) }).success, - ).toBe(true) - }) - - test('role は51文字を拒否する', () => { - expect( - scenarioCharacterSchema.safeParse({ ...minimalCharacter, role: 'x'.repeat(51) }).success, - ).toBe(false) - }) - - test('role が空白だけなら拒否する', () => { - expect(scenarioCharacterSchema.safeParse({ ...minimalCharacter, role: ' ' }).success).toBe( - false, - ) - }) - test('publicIntroduction が空白だけなら拒否する', () => { expect( scenarioCharacterSchema.safeParse({ ...minimalCharacter, publicIntroduction: ' ' }).success, @@ -674,7 +602,6 @@ describe('character の secrets / lies / memories / relationships', () => { expect( scenarioLieSchema.safeParse({ id: 'lie', - about: 'fact', claim: '嘘の主張', strategy: 'random', }).success, @@ -685,7 +612,6 @@ describe('character の secrets / lies / memories / relationships', () => { expect( scenarioLieSchema.safeParse({ id: 'lie', - about: 'fact', claim: ' ', strategy: 'maintain', }).success, @@ -726,12 +652,6 @@ describe('scenarioEvidenceSchema', () => { reveal: { condition: '証拠について尋ねる' }, } - test('reveal.mode を省略すると conversation になる', () => { - const result = scenarioEvidenceSchema.parse(minimalEvidence) - - expect(result.reveal.mode).toBe('conversation') - }) - test('supports を省略すると空配列になる', () => { const result = scenarioEvidenceSchema.parse(minimalEvidence) @@ -774,15 +694,6 @@ describe('scenarioEvidenceSchema', () => { .success, ).toBe(false) }) - - test('未知の reveal.mode を拒否する', () => { - expect( - scenarioEvidenceSchema.safeParse({ - ...minimalEvidence, - reveal: { mode: 'automatic', condition: '条件' }, - }).success, - ).toBe(false) - }) }) describe('scenarioSolutionSchema', () => { @@ -834,18 +745,6 @@ describe('scenarioSolutionSchema', () => { ) }) - test('requiredFacts が0件なら拒否する', () => { - expect( - scenarioSolutionSchema.safeParse({ ...minimalSolution, requiredFacts: [] }).success, - ).toBe(false) - }) - - test('requiredFacts の空IDを拒否する', () => { - expect( - scenarioSolutionSchema.safeParse({ ...minimalSolution, requiredFacts: [''] }).success, - ).toBe(false) - }) - test('secretKeywords が0件なら拒否する', () => { expect( scenarioSolutionSchema.safeParse({ ...minimalSolution, secretKeywords: [] }).success, @@ -859,73 +758,6 @@ describe('scenarioSolutionSchema', () => { }) }) -describe('scenarioQualitySchema', () => { - test('空オブジェクトなら redHerrings が空配列になる', () => { - const result = scenarioQualitySchema.parse({}) - - expect(result.redHerrings).toEqual([]) - }) - - test('expectedQuestionCount は min < max を受理する', () => { - const scenario = makeScenario() - scenario.quality.expectedQuestionCount = { min: 4, max: 12 } - - expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(true) - }) - - test('expectedQuestionCount は min = max を受理する', () => { - const scenario = makeScenario() - scenario.quality.expectedQuestionCount = { min: 5, max: 5 } - - expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(true) - }) - - test('expectedQuestionCount は min > max を拒否する', () => { - const scenario = makeScenario() - scenario.quality.expectedQuestionCount = { min: 13, max: 12 } - - expectInvalidAt(scenario, 'quality.expectedQuestionCount') - }) - - test('expectedQuestionCount は0を受理する', () => { - expect( - scenarioQualitySchema.safeParse({ expectedQuestionCount: { min: 0, max: 0 } }).success, - ).toBe(true) - }) - - test('expectedQuestionCount の負数を拒否する', () => { - expect( - scenarioQualitySchema.safeParse({ expectedQuestionCount: { min: -1, max: 1 } }).success, - ).toBe(false) - }) - - test('expectedQuestionCount の小数を拒否する', () => { - expect( - scenarioQualitySchema.safeParse({ expectedQuestionCount: { min: 1.5, max: 2 } }).success, - ).toBe(false) - }) - - test('requiredEvidence.min は0を受理する', () => { - expect(scenarioQualitySchema.safeParse({ requiredEvidence: { min: 0 } }).success).toBe(true) - }) - - test('requiredEvidence.min の負数を拒否する', () => { - expect(scenarioQualitySchema.safeParse({ requiredEvidence: { min: -1 } }).success).toBe(false) - }) - - test('requiredEvidence.min の小数を拒否する', () => { - expect(scenarioQualitySchema.safeParse({ requiredEvidence: { min: 1.5 } }).success).toBe(false) - }) - - test('redHerrings の空IDを拒否する', () => { - expect(scenarioQualitySchema.safeParse({ redHerrings: [''] }).success).toBe(false) - }) - - test('notes が空白だけなら拒否する', () => { - expect(scenarioQualitySchema.safeParse({ notes: ' ' }).success).toBe(false) - }) -}) - describe('semantic validation: ID の一意性', () => { test('fact ID の重複を拒否する', () => { const scenario = makeScenario() @@ -1006,13 +838,6 @@ describe('semantic validation: fact / character / lie の参照整合性', () => expectInvalidAt(scenario, 'characters.1.lies.0.about') }) - test('memory.about の存在しない fact を拒否する', () => { - const scenario = makeScenario() - requiredAt(requiredAt(scenario.characters, 0).memories, 0).about = 'missing-fact' - - expectInvalidAt(scenario, 'characters.0.memories.0.about') - }) - test('relationship の存在しない character を拒否する', () => { const scenario = makeScenario() requiredAt(requiredAt(scenario.characters, 0).relationships, 0).character = 'ghost' @@ -1068,13 +893,6 @@ describe('semantic validation: fact / character / lie の参照整合性', () => expectInvalidAt(scenario, 'solution.culprit') }) - - test('solution.requiredFacts の存在しない fact を拒否する', () => { - const scenario = makeScenario() - scenario.solution.requiredFacts.push('missing-fact') - - expectInvalidAt(scenario, 'solution.requiredFacts.2') - }) }) describe('scenarioRevelationSchema', () => { @@ -1404,13 +1222,6 @@ describe('semantic validation: 秘匿キーワード漏洩', () => { expectInvalidAt(scenario, 'solution.secretKeywords.0') }) - test('tags に秘匿キーワードが含まれていたら拒否する', () => { - const scenario = makeScenario() - scenario.meta.tags.push(requiredAt(scenario.solution.secretKeywords, 0)) - - expectInvalidAt(scenario, 'solution.secretKeywords.0') - }) - test('publicIntroduction に秘匿キーワードが含まれていたら拒否する', () => { const scenario = makeScenario() requiredAt(scenario.characters, 0).publicIntroduction = @@ -1422,7 +1233,7 @@ describe('semantic validation: 秘匿キーワード漏洩', () => { test('英字は大文字小文字を無視して漏洩検出する', () => { const scenario = makeScenario() scenario.solution.secretKeywords = ['SECRET-ANSWER'] - scenario.meta.tags.push('secret-answer') + scenario.meta.synopsis = `${scenario.meta.synopsis} secret-answer` expectInvalidAt(scenario, 'solution.secretKeywords.0') }) @@ -1474,3 +1285,64 @@ describe('ScenarioDefinitionSchema: evidence の sources', () => { expect(parsed.success ? requiredAt(parsed.data.evidences, 0).sources : undefined).toEqual([]) }) }) + +describe('被害者を出どころにする', () => { + test('type: victim は id が victim なら通る', () => { + const scenario = makeScenario() + scenario.victim = { name: '被害者', introduction: '館の主', findings: [] } + const evidence = scenario.evidences[0] + + if (evidence === undefined) { + throw new Error('この試験は証拠が1件以上ある前提で組んである。') + } + + evidence.sources = [{ type: 'victim', id: VICTIM_ID }] + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(true) + }) + + test('被害者の居ない事件では使えない', () => { + const scenario = makeScenario() + scenario.victim = undefined + const evidence = scenario.evidences[0] + + if (evidence === undefined) { + throw new Error('この試験は証拠が1件以上ある前提で組んである。') + } + + evidence.sources = [{ type: 'victim', id: VICTIM_ID }] + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(false) + }) + + test('id は victim で固定', () => { + const scenario = makeScenario() + scenario.victim = { name: '被害者', introduction: '館の主', findings: [] } + const evidence = scenario.evidences[0] + + if (evidence === undefined) { + throw new Error('この試験は証拠が1件以上ある前提で組んである。') + } + + evidence.sources = [{ type: 'victim', id: 'ryoko' }] + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(false) + }) + + test('所見の解禁前提は実在する証拠しか指せない', () => { + const scenario = makeScenario() + scenario.victim = { + name: '被害者', + introduction: '館の主', + findings: [ + { + id: 'draft', + statement: '草案が伏せてある。', + requires: { revelations: [], evidences: ['no-such-evidence'] }, + }, + ], + } + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(false) + }) +}) diff --git a/__tests__/db/scenario-place.test.ts b/__tests__/db/scenario-place.test.ts new file mode 100644 index 0000000..7438a6f --- /dev/null +++ b/__tests__/db/scenario-place.test.ts @@ -0,0 +1,407 @@ +import { describe, expect, test } from 'bun:test' +import { compileScenario } from '~/db/compile-scenario' +import { findingsOfPlace, parseInvestigablePlaces } from '~/db/place' +import { + type ScenarioDefinitionInput, + ScenarioDefinitionSchema, + VICTIM_ID, +} from '~/db/scenario-definition' +import { loadScenarioYaml } from '~/db/scenario-file' + +/** + * 調べられる場所。遺体の二人目として、同じ道に乗っているかを見る。 + * + * 検査したいのは三点。書ける形になっているか、公開と真相の境で二つに割れているか、 + * そして参照(`type: location`)が場所にも当たるか。 + */ + +const TSUKIMISOU_SCENARIO = await loadScenarioYaml('tsukimisou') + +/** 採番を決定的にする。実物の crypto.randomUUID では期待値が書けない。 */ +const sequentialIds = () => { + const state = { issued: 0 } + + return () => { + state.issued += 1 + return `id-${state.issued}` + } +} + +/** + * 最小のシナリオ。場所だけを足し引きして試すための土台で、 + * 実物の事件を改変して作ると、直したい条件以外の検査に先に引っかかる。 + */ +const makeMinimal = (): ScenarioDefinitionInput => ({ + schemaVersion: 1, + id: 'minimal-place-case', + meta: { + title: '最小の事件', + synopsis: '何かが起きた。', + category: 'テスト', + difficulty: 1, + estimatedMinutes: 5, + }, + briefing: '何かが起きたらしい。', + floorPlan: null, + facts: [{ id: 'fact-open', statement: '誰でも知っている事実', kind: 'observation' }], + timeline: [{ id: 'only-event', at: '12:00', participants: ['alpha'], facts: ['fact-open'] }], + characters: [ + { + id: 'alpha', + name: 'アルファ', + publicIntroduction: '設備担当のアルファ。', + personality: '淡々としている。', + goals: ['疑いを晴らす'], + knowledge: ['fact-open'], + secrets: [], + lies: [], + memories: [], + relationships: [], + }, + { + id: 'beta', + name: 'ベータ', + publicIntroduction: '受付担当のベータ。', + personality: 'よく喋る。', + goals: ['早く帰りたい'], + knowledge: ['fact-open'], + secrets: [], + lies: [], + memories: [], + relationships: [], + }, + ], + revelations: [], + evidences: [], + solution: { + culprit: 'alpha', + summary: 'アルファがやった。', + method: '鈍器で殴った。', + motive: '金銭トラブル。', + secretKeywords: ['アルファがやった'], + }, +}) + +const CHOBA = { + id: 'choba', + name: '帳場', + shortName: '帳場', + introduction: '一階。レジと帳面', + situation: '閉店の片づけが、途中で止まっている', + findings: [{ id: 'ledger-stopped', statement: '帳面は18時44分の記入で止まっている。' }], +} + +const withPlaces = (places: unknown[]): unknown => ({ ...makeMinimal(), places }) + +const issuesOf = (definition: unknown): string[] => { + const parsed = ScenarioDefinitionSchema.safeParse(definition) + + return parsed.success ? [] : parsed.error.issues.map((issue) => issue.message) +} + +describe('places: 書ける形', () => { + test('場所を持たない事件はそのまま通る', () => { + // 場所より前に書かれた事件を落とさないための既定。 + const parsed = ScenarioDefinitionSchema.safeParse(makeMinimal()) + + expect(parsed.success).toBe(true) + expect(parsed.success ? parsed.data.places : undefined).toEqual([]) + }) + + test('場所を足しても通る', () => { + expect(issuesOf(withPlaces([CHOBA]))).toEqual([]) + }) + + test('所見の無い場所は書けない', () => { + // 調べても何も出ない相手を並べると、一手ぶんの質問がそのまま無駄になる。 + expect(issuesOf(withPlaces([{ ...CHOBA, findings: [] }]))).not.toEqual([]) + }) + + test('同じ ID の場所は二つ置けない', () => { + const issues = issuesOf(withPlaces([CHOBA, { ...CHOBA, name: '奥の間' }])) + + expect(issues.join('\n')).toContain('place ID「choba」が重複しています。') + }) + + test('遺体を指す ID は場所に使えない', () => { + // ask の相手は人物・遺体・場所が同じ一つの口へ来る。重なると誰を指したのか決まらない。 + const issues = issuesOf(withPlaces([{ ...CHOBA, id: VICTIM_ID }])) + + expect(issues.join('\n')).toContain(VICTIM_ID) + }) + + test('uuid の形をした ID は場所に使えない', () => { + // 16進とハイフンだけの ID は、人物の ID と見分けが付かなくなる。 + const issues = issuesOf(withPlaces([{ ...CHOBA, id: 'e9b41c07-2d58-4a36-9f10-6c3b7a5d8e21' }])) + + expect(issues.join('\n')).toContain('uuid') + }) + + test('所見の解禁前提は実在する証拠しか指せない', () => { + const issues = issuesOf( + withPlaces([ + { + ...CHOBA, + findings: [ + { + id: 'later', + statement: '後から意味が変わる所見。', + requires: { evidences: ['nonexistent'], revelations: [] }, + }, + ], + }, + ]), + ) + + expect(issues.join('\n')).toContain('存在しない evidence「nonexistent」') + }) +}) + +describe('places: type: location の行き先', () => { + const evidence = { + id: 'ledger', + label: '帳面', + reveal: { condition: '帳場を調べたら開示する。' }, + sources: [{ type: 'location', id: 'choba' }], + supports: [], + contradicts: [], + } + + test('図面の無い事件でも、場所を出どころにできる', () => { + /* + 以前は部屋IDだけが照合先だったので、図を持たない事件では location のソースが + どこにも当たらなかった。場所が増えた以上、そちらにも当たる必要がある。 + */ + expect(issuesOf({ ...withPlaces([CHOBA]), evidences: [evidence] })).toEqual([]) + }) + + test('場所にも部屋にも無い ID は落ちる', () => { + const issues = issuesOf({ + ...withPlaces([CHOBA]), + evidences: [{ ...evidence, sources: [{ type: 'location', id: 'oku' }] }], + }) + + expect(issues.join('\n')).toContain('存在しない location「oku」') + }) +}) + +describe('places: 秘匿キーワードの検査', () => { + test('場所の紹介文は公開情報として扱われる', () => { + // 名簿には調べる前から並ぶ。ここに答えを書けば、聞き込みが始まる前に漏れる。 + const issues = issuesOf(withPlaces([{ ...CHOBA, introduction: 'アルファがやった現場' }])) + + expect(issues.join('\n')).toContain('秘匿キーワード') + }) + + test('所見は公開情報ではない', () => { + // 調べて初めて出るもの。遺体の findings と同じ扱い。 + const issues = issuesOf( + withPlaces([ + { + ...CHOBA, + findings: [{ id: 'ledger-stopped', statement: 'アルファがやった、と読める覚え書き。' }], + }, + ]), + ) + + expect(issues.join('\n')).not.toContain('秘匿キーワード') + }) +}) + +describe('places: コンパイル', () => { + const compiled = compileScenario( + { + ...withPlaces([CHOBA]), + evidences: [ + { + id: 'ledger', + label: '帳面', + reveal: { condition: '帳場を調べたら開示する。' }, + sources: [{ type: 'location', id: 'choba' }], + supports: [], + contradicts: [], + }, + ], + }, + { isPublished: true, newId: sequentialIds() }, + ) + + if (!compiled.ok) { + throw new Error(`コンパイルに失敗しました:\n${compiled.issues.join('\n')}`) + } + + test('公開側には調べる前から見せてよいものだけが入る', () => { + expect(compiled.compiled.scenario.places).toEqual([ + { + id: 'choba', + name: '帳場', + shortName: '帳場', + introduction: '一階。レジと帳面', + situation: '閉店の片づけが、途中で止まっている', + }, + ]) + }) + + test('所見は真相側へ分かれる', () => { + // 公開側に混ざると、調べる前に画面から読めてしまう。 + expect(JSON.stringify(compiled.compiled.scenario.places)).not.toContain('18時44分') + expect(compiled.compiled.truth.placeFindings).toEqual([ + { + placeId: 'choba', + findings: [ + { + id: 'ledger-stopped', + statement: '帳面は18時44分の記入で止まっている。', + requires: { revelations: [], evidences: [] }, + }, + ], + }, + ]) + }) + + test('場所の ID はローカルのまま。証拠のソースと突き合わせられる', () => { + /* + 人物だけが uuid へ振り替わる。場所を振り替えると、`type: location` のソースが + 指す先と食い違って、証拠がどこにも紐づかなくなる(部屋IDと同じ理由)。 + */ + const evidence = compiled.compiled.evidences[0] + + expect(evidence?.sources).toEqual([{ type: 'location', id: 'choba' }]) + }) +}) + +describe('places: 解禁前提の採番', () => { + test('所見の前提にある証拠IDは uuid へ振り替わる', () => { + /* + DO が持っている発見済みの ID は uuid。ローカルIDのまま焼くと、 + 前提が永久に満たされない所見になる。 + */ + const compiledWithRequires = compileScenario( + { + ...withPlaces([ + { + ...CHOBA, + findings: [ + { + id: 'later', + statement: '後から意味が変わる所見。', + requires: { evidences: ['ledger'], revelations: [] }, + }, + ], + }, + ]), + evidences: [ + { + id: 'ledger', + label: '帳面', + reveal: { condition: '帳場を調べたら開示する。' }, + sources: [], + supports: [], + contradicts: [], + }, + ], + }, + { isPublished: true, newId: sequentialIds() }, + ) + + if (!compiledWithRequires.ok) { + throw new Error(`コンパイルに失敗しました:\n${compiledWithRequires.issues.join('\n')}`) + } + + const evidenceId = compiledWithRequires.compiled.evidences[0]?.id + const required = + compiledWithRequires.compiled.truth.placeFindings?.[0]?.findings[0]?.requires.evidences + + expect(evidenceId).toBeDefined() + expect(required).toEqual([evidenceId === undefined ? 'ローカルIDのまま' : evidenceId]) + }) +}) + +describe('places: 実物のシナリオ', () => { + const tsukimisou = compileScenario(TSUKIMISOU_SCENARIO, { + isPublished: true, + newId: sequentialIds(), + }) + + if (!tsukimisou.ok) { + throw new Error(`コンパイルに失敗しました:\n${tsukimisou.issues.join('\n')}`) + } + + const places = tsukimisou.compiled.scenario.places + const plan = tsukimisou.compiled.scenario.floorPlan + + if (places === undefined || plan === null || plan === undefined) { + throw new Error('月見荘は見取り図と調べられる場所の両方を持っている前提で組んである。') + } + + test('月見荘には調べられる場所がある', () => { + expect(places.map((place) => place.id)).toEqual(['garden', 'phone']) + }) + + test('場所の ID は見取り図の部屋IDと重ねてある', () => { + /* + 同じ場所を指しているなら一つの場所。`type: location` のソースが、 + 図の部屋にも調べる相手にも同時に当たる。 + */ + const roomIds = plan.rooms.map((room) => room.id) + + for (const place of places) { + expect(roomIds).toContain(place.id) + } + }) + + test('場所を出どころにした証拠がある', () => { + // 場所を調べて出るものが一つも無いと、増やした一手が空振りになる。 + const placeIds = new Set(places.map((place) => place.id)) + const fromPlaces = tsukimisou.compiled.evidences.filter((evidence) => + evidence.sources?.some((source) => source.type === 'location' && placeIds.has(source.id)), + ) + + expect(fromPlaces.length).toBeGreaterThan(0) + }) +}) + +describe('parseInvestigablePlaces', () => { + test('保存された形をそのまま読む', () => { + const stored = [ + { + id: 'choba', + name: '帳場', + shortName: '帳場', + introduction: '一階。レジと帳面', + situation: '片づけが途中で止まっている', + }, + ] + + expect(parseInvestigablePlaces(stored)).toEqual(stored) + }) + + test('読めない値は場所なしとして返す', () => { + // ここで投げると、場所が一つ壊れただけで事件そのものが開けなくなる。 + expect(parseInvestigablePlaces(undefined)).toEqual([]) + expect(parseInvestigablePlaces([{ id: 'choba' }])).toEqual([]) + }) +}) + +describe('findingsOfPlace', () => { + const all = [ + { + placeId: 'choba', + findings: [ + { + id: 'a', + statement: '帳面が止まっている。', + requires: { revelations: [], evidences: [] }, + }, + ], + }, + ] + + test('その場所の所見だけを返す', () => { + expect(findingsOfPlace(all, 'choba')).toHaveLength(1) + }) + + test('載っていない場所には所見が無い', () => { + expect(findingsOfPlace(all, 'oku')).toEqual([]) + }) +}) diff --git a/__tests__/db/scenario-public-layer.test.ts b/__tests__/db/scenario-public-layer.test.ts index 51f00b6..a5b78cb 100644 --- a/__tests__/db/scenario-public-layer.test.ts +++ b/__tests__/db/scenario-public-layer.test.ts @@ -15,6 +15,9 @@ const titleSpoilerPattern = const publicIntroductionSpoilerPattern = /(アリバイ|秘密|隠し|不正|証拠として|人物識別|思い込み|標準時|時系列|休憩延長|記録から外|機器表示|杖の音|絶対視|バッジ)/ +const placePublicSpoilerPattern = + /(自動施錠|閉じるだけで|反射|映り込|重量センサー|人物を識別|送信者を識別|十一分|時刻のずれ|自律飛行|繰り返す経路|実行ログ|電源履歴|通過履歴|新しい打痕|台紙片)/ + const scenarioFiles = async (): Promise => (await readdir(SCENARIO_DIR)).filter((name) => name.endsWith('.yaml')).sort() @@ -44,6 +47,13 @@ describe('scenario public layer', () => { violations.push(`${file}: publicIntroduction:${character.name}`) } } + + for (const place of scenario.places) { + const publicPlaceText = `${place.name}\n${place.shortName}\n${place.introduction}\n${place.situation}` + if (placePublicSpoilerPattern.test(publicPlaceText)) { + violations.push(`${file}: place:${place.id}`) + } + } } expect(violations).toEqual([]) diff --git a/__tests__/server/alibi.test.ts b/__tests__/server/alibi.test.ts new file mode 100644 index 0000000..4fec5b9 --- /dev/null +++ b/__tests__/server/alibi.test.ts @@ -0,0 +1,282 @@ +import { describe, expect, test } from 'bun:test' +import { + alibiSegmentsOf, + clashOf, + deathEstimateOf, + type LieRef, + type RevealedClues, +} from '@/server/game/alibi' +import type { TimelineEvent } from '~/db/timeline-event' + +const event = ( + id: string, + at: string, + participants: string[], + facts: string[], + kind: TimelineEvent['kind'] = 'solid', + record = '', +): TimelineEvent => ({ id, at, place: '', room: '', record, participants, facts, kind }) + +const MAKINO = 'makino-uuid' +const KURODA = 'kuroda-uuid' + +/** 三つの出来事。牧野は最初と最後、黒田は真ん中と最後に居合わせる。 */ +const EVENTS: TimelineEvent[] = [ + event('opens', '18:20', [MAKINO], ['store-open']), + event('visit', '18:41', [KURODA], ['kuroda-visited']), + event('found', '19:12', [MAKINO, KURODA], ['body-found']), +] + +const END = '19:30' + +const clues = (partial: Partial): RevealedClues => ({ + revelations: [], + evidenceSupports: [], + ...partial, +}) + +const segmentsOf = (given: Partial) => + alibiSegmentsOf({ events: EVENTS, end: END, clues: clues(given) }) + +describe('alibiSegmentsOf', () => { + test('何も掴んでいなければ、線は一本も引かれない', () => { + expect(segmentsOf({})).toEqual([]) + }) + + test('掴んだ手掛かりが触れている事実から、その出来事が開く', () => { + const segments = segmentsOf({ + revelations: [{ subjectType: 'character', subjectId: MAKINO, relatedFacts: ['store-open'] }], + }) + + expect(segments).toHaveLength(1) + expect(segments[0]).toMatchObject({ who: MAKINO, from: '18:20', to: END }) + }) + + test('出来事を名指しした revelation は、その出来事を直に開く', () => { + const segments = segmentsOf({ + revelations: [{ subjectType: 'event', subjectId: 'visit', relatedFacts: [] }], + }) + + expect(segments.map((s) => s.who)).toEqual([KURODA]) + }) + + test('証拠が裏付ける事実でも開く', () => { + const segments = segmentsOf({ evidenceSupports: ['body-found'] }) + + expect(segments.map((s) => s.who).toSorted()).toEqual([KURODA, MAKINO].toSorted()) + }) + + /* + 未発見の出来事を線の終わりに使うと、まだ知らないはずの時刻が + 線の長さとして漏れる。牧野の 18:20 の線は、牧野について次に分かっている + 19:12 まで伸びるのが正しい。 + */ + test('線の終わりは、その人について次に分かっている出来事まで', () => { + const segments = segmentsOf({ evidenceSupports: ['store-open', 'body-found'] }) + const makino = segments.filter((s) => s.who === MAKINO) + + expect(makino).toHaveLength(2) + expect(makino[0]).toMatchObject({ from: '18:20', to: '19:12' }) + expect(makino[1]).toMatchObject({ from: '19:12', to: END }) + }) + + test('知る前の線は、知らない区間をまたいで伸びている', () => { + const before = segmentsOf({ evidenceSupports: ['store-open'] }) + + expect(before[0]).toMatchObject({ from: '18:20', to: END }) + }) + + test('裏付けのある線にだけ時刻の印が付く', () => { + const [solid] = segmentsOf({ evidenceSupports: ['store-open'] }) + const [claim] = alibiSegmentsOf({ + events: [event('hearsay', '18:20', [MAKINO], ['said-so'], 'claim')], + end: END, + clues: clues({ evidenceSupports: ['said-so'] }), + }) + + expect(solid).toMatchObject({ kind: 'solid', fix: '18:20' }) + expect(claim?.kind).toBe('claim') + expect(claim?.fix).toBeUndefined() + }) + + test('記録の名前があれば、時刻に添えて札になる', () => { + const segments = alibiSegmentsOf({ + events: [event('receipt', '19:08', [MAKINO], ['receipt-fact'], 'solid', '受付')], + end: END, + clues: clues({ evidenceSupports: ['receipt-fact'] }), + }) + + expect(segments[0]?.fix).toBe('19:08 受付') + }) + + test('記録の名前が無ければ、札は時刻だけ', () => { + const [solid] = segmentsOf({ evidenceSupports: ['store-open'] }) + + expect(solid?.fix).toBe('18:20') + }) + + test('申告だけの線には、記録の名前があっても札を付けない', () => { + const segments = alibiSegmentsOf({ + events: [event('hearsay', '18:20', [MAKINO], ['said-so'], 'claim', '本人談')], + end: END, + clues: clues({ evidenceSupports: ['said-so'] }), + }) + + expect(segments[0]?.fix).toBeUndefined() + }) + + test('幕切れに重なる出来事は線にならない', () => { + const segments = alibiSegmentsOf({ + events: [event('late', END, [MAKINO], ['late-fact'])], + end: END, + clues: clues({ evidenceSupports: ['late-fact'] }), + }) + + expect(segments).toEqual([]) + }) +}) + +/* + 食い違いの印。証拠が嘘を突き崩したとき、その嘘が言い張っていた時刻に立つ。 +*/ +describe('clashOf', () => { + const SENA = 'sena-uuid' + + const LIES: LieRef[] = [ + { id: 'makino-left-early', about: 'store-open', who: MAKINO }, + { id: 'kuroda-went-home', about: 'kuroda-visited', who: KURODA }, + ] + + /** 瀬名から得た証拠。人物の出所があるので、印のもう片方の端になれる。 */ + const fromSena = (contradicts: string[]) => [ + { contradicts, sources: [{ type: 'character', id: SENA }] }, + ] + + const clashWith = (contradicts: string[]) => + clashOf({ events: EVENTS, lies: LIES, evidences: fromSena(contradicts) }) + + test('崩された嘘が無ければ、印は立たない', () => { + expect(clashWith([])).toBeUndefined() + }) + + test('証拠が嘘を突き崩すと、その嘘が言い張っていた時刻に立つ', () => { + expect(clashWith(['lie:kuroda-went-home'])).toEqual({ + at: '18:41', + label: '食い違い', + between: [KURODA, SENA], + }) + }) + + test('複数崩れても、印はいちばん早い時刻の一つだけ', () => { + expect(clashWith(['lie:kuroda-went-home', 'lie:makino-left-early'])).toEqual({ + at: '18:20', + label: '食い違い', + between: [MAKINO, SENA], + }) + }) + + test('存在しない嘘を指していても、印は立たない', () => { + expect(clashWith(['lie:no-such-lie'])).toBeUndefined() + }) + + /* contradicts は `lie:` の形だけを見る。将来ほかの接頭辞が増えても取り違えない。 */ + test('lie: 以外の書き方は読まない', () => { + expect(clashWith(['kuroda-went-home'])).toBeUndefined() + }) + + test('嘘が指す事実がどの出来事にも無ければ、時刻が決まらないので立たない', () => { + const orphan: LieRef[] = [{ id: 'orphan', about: 'fact-without-event', who: MAKINO }] + + expect( + clashOf({ events: EVENTS, lies: orphan, evidences: fromSena(['lie:orphan']) }), + ).toBeUndefined() + }) + + /* + 崩した側が誰か分からなければ、線を架ける先が無い。場所や遺体から出た証拠は + 表に列を持たないので、ここでは端になれない。 + */ + test('人物の出所を持たない証拠だけでは、印は立たない', () => { + expect( + clashOf({ + events: EVENTS, + lies: LIES, + evidences: [ + { contradicts: ['lie:kuroda-went-home'], sources: [{ type: 'location', id: 'store' }] }, + ], + }), + ).toBeUndefined() + }) + + /* 自分の嘘を自分で崩す証拠は端にならない。線の幅が消えて、印が一本の柱に潰れる。 */ + test('崩した出所が嘘の主と同じなら、二人にならないので立たない', () => { + expect( + clashOf({ + events: EVENTS, + lies: LIES, + evidences: [ + { contradicts: ['lie:kuroda-went-home'], sources: [{ type: 'character', id: KURODA }] }, + ], + }), + ).toBeUndefined() + }) + + /* + 時刻と二人は同じ嘘から取る。別々に選ぶと、牧野の嘘が言い張る時刻に + 黒田と瀬名の線が架かる——誰も言っていないことを盤面が言い出す。 + */ + test('印の時刻と二人は、同じ嘘から出る', () => { + const clash = clashOf({ + events: EVENTS, + lies: LIES, + evidences: [ + { contradicts: ['lie:makino-left-early'], sources: [{ type: 'character', id: SENA }] }, + { contradicts: ['lie:kuroda-went-home'], sources: [{ type: 'character', id: MAKINO }] }, + ], + }) + + expect(clash).toEqual({ at: '18:20', label: '食い違い', between: [MAKINO, SENA] }) + }) +}) + +describe('deathEstimateOf', () => { + /* 事件の記録が語っているのは発見時刻だけ。死亡推定は手に入れるまで盤面に出さない。 */ + test('印の付いた証拠を掴むまでは開かない', () => { + expect( + deathEstimateOf({ + estimatedDeathAt: '18:50', + evidences: [{ revealsDeathTime: false }, { revealsDeathTime: false }], + }), + ).toBeNull() + }) + + test('何も掴んでいなければ、当然まだ開かない', () => { + expect(deathEstimateOf({ estimatedDeathAt: '18:50', evidences: [] })).toBeNull() + }) + + test('印の付いた証拠が一つでもあれば時刻が出る', () => { + expect( + deathEstimateOf({ + estimatedDeathAt: '18:50', + evidences: [{ revealsDeathTime: false }, { revealsDeathTime: true }], + }), + ).toBe('18:50') + }) + + /* 検死からでも医師の見立てからでも、辿り着く先は同じ一つの時刻。 */ + test('道が二つ開いていても、返るのは一つの時刻', () => { + expect( + deathEstimateOf({ + estimatedDeathAt: '18:50', + evidences: [{ revealsDeathTime: true }, { revealsDeathTime: true }], + }), + ).toBe('18:50') + }) + + /* 死亡推定時刻を書いていない事件では、印があっても出す時刻が無い。 */ + test('時刻を持たない事件では、印があっても null', () => { + expect( + deathEstimateOf({ estimatedDeathAt: null, evidences: [{ revealsDeathTime: true }] }), + ).toBeNull() + }) +}) diff --git a/__tests__/server/examination.test.ts b/__tests__/server/examination.test.ts new file mode 100644 index 0000000..e4a3b42 --- /dev/null +++ b/__tests__/server/examination.test.ts @@ -0,0 +1,204 @@ +import { describe, expect, test } from 'bun:test' +import { + buildPlaceSheet, + buildVictimSheet, + type PlaceRecord, + type VictimRecord, +} from '@/server/game/examination' +import { availableFindings, type VictimFinding } from '~/db/victim-finding' + +const finding = ( + id: string, + statement: string, + requires?: VictimFinding['requires'], +): VictimFinding => ({ + id, + statement, + requires: requires === undefined ? { revelations: [], evidences: [] } : requires, +}) + +const NOTHING = { evidenceIds: [], revelationIds: [] } + +const record = (overrides: Partial): VictimRecord => ({ + name: '高瀬涼子', + introduction: '月見荘女将', + briefing: '——事件の記録を読み上げます。', + foundAt: '20:30', + foundIn: '書斎', + causeOfDeath: null, + findings: [], + ...overrides, +}) + +describe('availableFindings', () => { + test('前提のない所見はそのまま出る', () => { + const findings = [finding('a', '争った跡が無い。')] + + expect(availableFindings(findings, NOTHING)).toHaveLength(1) + }) + + test('前提を満たしていない所見は落ちる', () => { + const findings = [ + finding('a', '争った跡が無い。'), + finding('b', '草案が伏せてある。', { revelations: [], evidences: ['will-record'] }), + ] + + expect(availableFindings(findings, NOTHING).map((item) => item.id)).toEqual(['a']) + }) + + test('前提を満たせば出る', () => { + const findings = [ + finding('b', '草案が伏せてある。', { revelations: [], evidences: ['will-record'] }), + ] + const discovered = { evidenceIds: ['will-record'], revelationIds: [] } + + expect(availableFindings(findings, discovered).map((item) => item.id)).toEqual(['b']) + }) + + test('前提が複数あるときは全部揃って初めて出る', () => { + const findings = [ + finding('b', '草案が伏せてある。', { revelations: ['motive'], evidences: ['will-record'] }), + ] + + expect( + availableFindings(findings, { evidenceIds: ['will-record'], revelationIds: [] }), + ).toEqual([]) + }) +}) + +describe('buildVictimSheet', () => { + test('所見も死因も無ければ組まない', () => { + // 空のシートを渡すと、モデルが埋めようとして所見を作りはじめる。 + expect(buildVictimSheet(record({}), NOTHING)).toBeUndefined() + }) + + test('死因だけでも組む', () => { + const sheet = buildVictimSheet(record({ causeOfDeath: '中毒死' }), NOTHING) + + expect(sheet).toContain('中毒死') + expect(sheet).toContain('20:30') + expect(sheet).toContain('書斎') + }) + + test('伏せた所見は本文に現れない', () => { + const sheet = buildVictimSheet( + record({ + causeOfDeath: '中毒死', + findings: [ + finding('a', '争った跡が無い。'), + finding('b', '草案が伏せてある。', { revelations: [], evidences: ['will-record'] }), + ], + }), + NOTHING, + ) + + expect(sheet).toContain('争った跡が無い。') + expect(sheet).not.toContain('草案') + }) + + test('伏せた所見があること自体も漏らさない', () => { + // 「まだ何かある」と分かると、前提を満たす前に答えの形が見えてしまう。 + const sheet = buildVictimSheet( + record({ + causeOfDeath: '中毒死', + findings: [finding('b', '草案。', { revelations: [], evidences: ['will-record'] })], + }), + NOTHING, + ) + + expect(sheet).not.toContain('will-record') + expect(sheet).not.toMatch(/非公開|伏せ|まだ見せ/) + }) + + test('分かっていないことは行ごと出さない', () => { + const sheet = buildVictimSheet( + record({ foundAt: null, foundIn: null, findings: [finding('a', '争った跡が無い。')] }), + NOTHING, + ) + + expect(sheet).not.toContain('発見時刻') + expect(sheet).not.toContain('発見場所') + }) +}) + +const place = (overrides: Partial): PlaceRecord => ({ + name: '帳場', + introduction: '青雨堂の一階。レジと帳面', + situation: '閉店の片づけが、途中で止まっている', + briefing: '——事件の記録を読み上げます。', + findings: [], + ...overrides, +}) + +describe('buildPlaceSheet', () => { + test('所見が無ければ組まない', () => { + /* + 佇まいの一行だけを渡すと、モデルはそこから所見を作りはじめる。 + 一行の情景描写は、埋めるための余白として十分に広い。 + */ + expect(buildPlaceSheet(place({}), NOTHING)).toBeUndefined() + }) + + test('所見と佇まいを並べて組む', () => { + const sheet = buildPlaceSheet( + place({ findings: [finding('a', '帳面は18時44分で止まっている。')] }), + NOTHING, + ) + + expect(sheet).toContain('帳場') + expect(sheet).toContain('閉店の片づけが、途中で止まっている') + expect(sheet).toContain('18時44分') + }) + + test('遺体の見出しを持ち込まない', () => { + // 倒れている人を見るのと、片づけの途中の帳場を見るのは別の行為である。 + const sheet = buildPlaceSheet(place({ findings: [finding('a', '帳面。')] }), NOTHING) + + expect(sheet).not.toContain('死因') + expect(sheet).not.toContain('被害者') + expect(sheet).not.toContain('遺体') + }) + + test('伏せた所見は本文に現れない', () => { + const sheet = buildPlaceSheet( + place({ + findings: [ + finding('a', '帳面は18時44分で止まっている。'), + finding('b', '棚に隙間がある。', { revelations: [], evidences: ['forged-book'] }), + ], + }), + NOTHING, + ) + + expect(sheet).toContain('18時44分') + expect(sheet).not.toContain('隙間') + }) + + test('伏せた所見があること自体も漏らさない', () => { + const sheet = buildPlaceSheet( + place({ + findings: [ + finding('a', '帳面は18時44分で止まっている。'), + finding('b', '棚に隙間がある。', { revelations: [], evidences: ['forged-book'] }), + ], + }), + NOTHING, + ) + + expect(sheet).not.toContain('forged-book') + expect(sheet).not.toMatch(/非公開|伏せ|まだ見せ/) + }) + + test('前提を満たせば所見が増える', () => { + const sheet = buildPlaceSheet( + place({ + findings: [ + finding('b', '棚に隙間がある。', { revelations: [], evidences: ['forged-book'] }), + ], + }), + { evidenceIds: ['forged-book'], revelationIds: [] }, + ) + + expect(sheet).toContain('隙間') + }) +}) diff --git a/__tests__/server/hints.test.ts b/__tests__/server/hints.test.ts index 8930ed3..1a207ca 100644 --- a/__tests__/server/hints.test.ts +++ b/__tests__/server/hints.test.ts @@ -182,3 +182,78 @@ describe('gameModeOf', () => { expect(gameModeOf('impossible')).toBe('nohope') }) }) + +/* + * 遺体由来の手掛かり。 + * + * 出どころの type は解禁の判定では victim だが、数える側では人物へ畳んである + * (src/server/cache/scenario.ts の asHintSource)。画面でも聴く相手の並びに + * 一人分として出るので、そこだけ別枠にすると内訳と実際の出方が食い違う。 + */ +describe('remainingHints: 遺体を数える相手に含めたとき', () => { + const withVictim: HintItem[] = [ + ...items, + { id: 'nail-fiber', sources: [{ type: 'character', id: 'victim' }] }, + { + id: 'will-draft', + sources: [ + { type: 'character', id: 'victim' }, + { type: 'character', id: 'mizuki' }, + ], + }, + ] + + test('内訳に遺体の行が出る', () => { + const hint = remainingHints({ + mode: 'easy', + items: withVictim, + discoveredIds: [], + roomIds, + characterIds: [...characterIds, 'victim'], + }) + const victim = hint.mode === 'easy' ? hint.characters.find((c) => c.id === 'victim') : undefined + + expect(victim).toEqual({ id: 'victim', remaining: 2 }) + }) + + test('調べられない事件では並びに出さない(数える相手に入れない)', () => { + // 聞き込みの相手に出てこない相手へ「あと0件」と添えるのは、無いものを数えて見せることになる。 + const hint = remainingHints({ + mode: 'easy', + items: withVictim, + discoveredIds: [], + roomIds, + characterIds, + }) + const ids = hint.mode === 'easy' ? hint.characters.map((c) => c.id) : [] + + expect(ids).not.toContain('victim') + }) + + test('遺体から掴んだ分は、遺体の行からも人物の行からも減る', () => { + const hint = remainingHints({ + mode: 'easy', + items: withVictim, + discoveredIds: ['will-draft'], + roomIds, + characterIds: [...characterIds, 'victim'], + }) + const characters = hint.mode === 'easy' ? hint.characters : [] + + // 美月は brandy も残しているので、減るのは will-draft のぶんだけ。 + expect(characters.find((c) => c.id === 'victim')?.remaining).toBe(1) + expect(characters.find((c) => c.id === 'mizuki')?.remaining).toBe(1) + }) + + test('normal の人数にも数えられている(畳んであるので type は character)', () => { + const hint = remainingHints({ + mode: 'normal', + items: withVictim, + discoveredIds: [], + roomIds, + characterIds: [...characterIds, 'victim'], + }) + + expect(hint).toEqual({ mode: 'normal', places: 2, people: 5 }) + }) +}) diff --git a/__tests__/server/scenario-public-introduction.test.ts b/__tests__/server/scenario-public-introduction.test.ts index 5d29a8b..75d7c80 100644 --- a/__tests__/server/scenario-public-introduction.test.ts +++ b/__tests__/server/scenario-public-introduction.test.ts @@ -13,3 +13,18 @@ describe('normalizePublicIntroduction', () => { expect(normalizePublicIntroduction(' ')).toBe('この事件の関係者。') }) }) + +import { sortCharactersById } from '@/server/read/scenarios' + +test('sortCharactersById > UUID順で決定的に並べ、元配列は変更しない', () => { + const input = [ + { id: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', name: 'C', publicIntroduction: 'C' }, + { id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', name: 'A', publicIntroduction: 'A' }, + { id: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', name: 'B', publicIntroduction: 'B' }, + ] + + const sorted = sortCharactersById(input) + + expect(sorted.map((character) => character.name)).toEqual(['A', 'B', 'C']) + expect(input.map((character) => character.name)).toEqual(['C', 'A', 'B']) +}) diff --git a/__tests__/shared/turns.test.ts b/__tests__/shared/turns.test.ts index aa12bad..f429ec9 100644 --- a/__tests__/shared/turns.test.ts +++ b/__tests__/shared/turns.test.ts @@ -135,11 +135,11 @@ describe('clampLimits', () => { }) /* - ターン数と1ターンの質問数を両方上限まで上げられると 30 問になる。 - 10分で遊ぶゲームの形が変わるので、積にも天井を置く。 + ターン数と1ターンの質問数を両方上限まで上げられると 45 問になる。 + 3〜5人の事件で全員に片端から聞けてしまうので、積にも天井を置く。 */ test('質問の総数が上限を超えない', () => { - const limits = clampLimits({ maxTurns: 10, questionsPerTurn: 3 }, fallback) + const limits = clampLimits({ maxTurns: 15, questionsPerTurn: 3 }, fallback) expect(limits.maxTurns * limits.questionsPerTurn).toBeLessThanOrEqual( LIMIT_CEILINGS.totalQuestions, @@ -147,9 +147,9 @@ describe('clampLimits', () => { }) test('積の天井に当たったらターン数ではなく1ターンの質問数を削る', () => { - const limits = clampLimits({ maxTurns: 10, questionsPerTurn: 3 }, fallback) + const limits = clampLimits({ maxTurns: 15, questionsPerTurn: 3 }, fallback) - expect(limits.maxTurns).toBe(10) + expect(limits.maxTurns).toBe(15) expect(limits.questionsPerTurn).toBe(2) }) diff --git a/db/author.ts b/db/author.ts index c957704..6e280d4 100644 --- a/db/author.ts +++ b/db/author.ts @@ -38,7 +38,14 @@ export type AuthorResult = * ファイルへ書くのは前者(既定値で膨らんでいない方が人が手を入れやすい)、 * id やタイトルを読むのは後者を使う。 */ - | { ok: true; definition: unknown; validated: ScenarioDefinition; attempts: AuthorAttempt[] } + | { + ok: true + definition: unknown + validated: ScenarioDefinition + /** 残ったまま採用した指摘。空とは限らない(authoringWarnings 参照)。 */ + warnings: string[] + attempts: AuthorAttempt[] + } | { ok: false; attempts: AuthorAttempt[] } /** @@ -48,6 +55,70 @@ export type AuthorResult = * 元の定義そのものは generate 側が previous.definition として持っているので、 * ここでは repeat しない。 */ +/** + * 検証は通るが、書けば画面が良くなること。 + * + * スキーマの側では落とせない。既存の43本はここを満たしていないものが多く、 + * 硬い検査にすると seed が丸ごと止まる。かといって黙っていると、Author LLM は + * 「省いても通るなら省く」ほうへ寄る。なので生成ループの中だけで差し戻し、 + * 回数を使い切ったら警告を付けたまま採用する(手順書 §6 と対になっている)。 + */ +export const authoringWarnings = (definition: ScenarioDefinition): string[] => { + const physicalFacts = new Set( + definition.facts.filter((fact) => fact.kind === 'physical').map((fact) => fact.id), + ) + + /* + 物証がその時刻を留めているのに、記録の名前が無い出来事。 + アリバイ表の目盛りが「19:08」という裸の数字になり、何がその時刻を + 決めたのかが画面から読めなくなる。observation だけの出来事は求めない + ——人が見ていただけの時刻に記録の名前は無い。 + */ + const missingRecords = definition.timeline + .filter( + (event) => event.record === undefined && event.facts.some((id) => physicalFacts.has(id)), + ) + .map( + (event) => + `timeline「${event.id}」は物証で裏付けられているのに record がありません。アリバイ表の目盛りが時刻だけになります。その時刻を留めた記録の名前を12文字までで付けてください。`, + ) + + /* + アリバイ表を横断する「食い違い」の印が、一度でも立てるか。 + + 印は src/server/game/alibi.ts の clashOf が決めていて、条件が三つ重なる。 + 証拠がその嘘を崩していること(contradicts)、崩れた嘘の about が timeline の + どれかの出来事の facts に載っていること、そして**崩した証拠に嘘の主とは別の + 人物の出所(sources の type: character)があること**。印は線を二本の柱の + あいだに架けるので、崩した側が誰か分からなければ架ける先が無い。 + どれか一つでも欠けると、矛盾を掴んだのに表が何も言わない事件ができあがる。 + + 嘘ごとには求めない——動機や身元の嘘は時刻表に載らないのが普通で、 + そこまで縛ると時刻と関係のない嘘が書けなくなる。事件を通して一本あればよい。 + */ + const timelineFacts = new Set(definition.timeline.flatMap((event) => event.facts)) + const hasClash = definition.characters.some((character) => + character.lies.some( + (lie) => + timelineFacts.has(lie.about) && + definition.evidences.some( + (evidence) => + evidence.contradicts.includes(`lie:${lie.id}`) && + evidence.sources.some( + (source) => source.type === 'character' && source.id !== character.id, + ), + ), + ), + ) + + return hasClash + ? missingRecords + : [ + ...missingRecords, + '証拠で崩せる嘘のうち「about が timeline の出来事の facts に載っていて、崩す証拠に嘘の主とは別の人物の出所がある」ものが一つもありません。このままではアリバイ表に「食い違い」の印が一度も立ちません。嘘が言い張っている事実を時刻表の出来事に含め、その嘘を崩す証拠に別の人物の出所(sources の type: character)を持たせてください。', + ] +} + export const describeIssues = (issues: string[]): string => `直前の出力には ${issues.length} 件の問題があります。すべて修正した完全な定義を出力してください。 @@ -69,14 +140,25 @@ export const runAuthor = async (options: { const definition = await options.generate({ premise: options.premise, previous }) const validated = validateScenario(definition) + const issues = validated.ok ? authoringWarnings(validated.definition) : validated.issues - if (validated.ok) { - return { ok: true, definition, validated: validated.definition, attempts: history } + /* + 警告だけのときは、直す機会が残っているあいだだけ差し戻す。 + 最後の一回で警告を理由に捨てると、検証を通る定義があるのに手ぶらで終わる。 + */ + if (validated.ok && (issues.length === 0 || remaining === 1)) { + return { + ok: true, + definition, + validated: validated.definition, + warnings: issues, + attempts: history, + } } - return attempt(remaining - 1, { definition, issues: validated.issues }, [ + return attempt(remaining - 1, { definition, issues }, [ ...history, - { attempt: history.length + 1, issues: validated.issues }, + { attempt: history.length + 1, issues }, ]) } diff --git a/db/compile-scenario.ts b/db/compile-scenario.ts index 3ec00ba..8b94ea3 100644 --- a/db/compile-scenario.ts +++ b/db/compile-scenario.ts @@ -3,10 +3,12 @@ import { type ScenarioDefinition, ScenarioDefinitionSchema, type ScenarioEvidenceSource, + type ScenarioFinding, type ScenarioRevelationSource, } from './scenario-definition' import type { characters, evidences, revelations, scenarios, scenarioTruths } from './schema' -import { timeWindowOf } from './time-window' +import { formatClock, minutesOf, timeWindowOf } from './time-window' +import { kindOfEvent } from './timeline-event' /** * Authoring 用のシナリオ定義を、実行時テーブルの行へ分解する。 @@ -27,7 +29,8 @@ type RevelationRow = typeof revelations.$inferInsert type TruthRow = typeof scenarioTruths.$inferInsert export type CompiledScenario = { - scenario: ScenarioRow + /** id は必ず決まっている(採番するか、呼び出し側が渡すか)。焼き直しの消去にこれを使う。 */ + scenario: ScenarioRow & { id: string } characters: CharacterRow[] evidences: EvidenceRow[] revelations: RevelationRow[] @@ -42,6 +45,13 @@ export type CompileScenarioOptions = { isPublished: boolean /** uuid の採番。テストから決定的な採番を差し込めるように注入で受け取る。 */ newId: () => string + /** + * シナリオ行のID。省略すると採番する。 + * + * seed が渡してくる。あちらは焼き直しのたびに同じIDを作り、そのIDで古い行を消してから + * 入れ直す——題名で消していた頃は、題名を変えた回の行が消えずに二重に残った。 + */ + scenarioId?: string } /** @@ -77,7 +87,7 @@ const compileDefinition = ( definition: ScenarioDefinition, options: CompileScenarioOptions, ): CompiledScenario => { - const scenarioId = options.newId() + const scenarioId = options.scenarioId === undefined ? options.newId() : options.scenarioId const characterIds = new Map( definition.characters.map((character) => [character.id, options.newId()]), @@ -93,6 +103,7 @@ const compileDefinition = ( definition.characters.map((character) => [character.id, character.name]), ) const factStatements = new Map(definition.facts.map((fact) => [fact.id, fact.statement])) + const factKinds = new Map(definition.facts.map((fact) => [fact.id, fact.kind])) /** * ローカルIDの引き当て。 @@ -174,7 +185,8 @@ const compileDefinition = ( })`, ), ), - // about は検証と追跡のための紐であって、読ませる情報ではない。detail だけ出す。 + // 散文にすると id と対象が消えるので、紐だけ別に残す。読ませる情報ではない。 + lieRefs: character.lies.map((lie) => ({ id: lie.id, about: lie.about })), memories: bullets(character.memories.map((memory) => memory.detail)), })) @@ -182,8 +194,21 @@ const compileDefinition = ( id: evidenceUuid(evidence.id), scenarioId, label: evidence.label, + // 捜査メモが読む。掴んだ証拠の中身が分からないと、記録がラベルの羅列になる。 + description: evidence.description === undefined ? null : evidence.description, revealCondition: evidence.reveal.condition, sources: evidence.sources.map(mapEvidenceSource), + // 時刻表が読む。証拠は revelation より頻繁に見つかるので、 + // これを落とすと発見しても線がほとんど増えない。 + supports: evidence.supports, + // 食い違いの印が読む。どの嘘を崩したかが分かって初めて、盤面の一点を指せる。 + contradicts: evidence.contradicts, + /* + 刻限が読む。この証拠を掴んだ瞬間に死亡推定が盤面へ出る、という印。 + 時刻そのものは公開側(scenarios.victimEstimatedDeathAt)にあり、 + サーバは掴んだ証拠にこの印があるときだけそれを返す。 + */ + revealsDeathTime: evidence.revealsDeathTime, })) const compiledRevelations = definition.revelations.map((revelation) => ({ @@ -220,6 +245,41 @@ const compileDefinition = ( : event.description, })) + /* + 同じ出来事を、時刻表が読める構造のまま別列へ。読み物(上の timeline)と + 盤面は求めるものが違うので、片方を潰してもう片方に使わせない。 + + `at` をここで HH:mm へ揃えるのは、authoring が ISO 8601 も許しているため。 + 時刻表は分単位でしか読まないので、読む側ごとに書式を判定させる理由が無い。 + 揃えられない書式(ここに来る時点でスキーマは通っている)は落とす—— + 軸に置けない線を持っていても、描く段で困るだけ。 + + 在所(location)は画面にそのまま出る文字で、部屋との紐付けは room が別に持つ。 + 以前は location が両方を兼ねていて、部屋IDのまま焼くと表に「study」と英字が並んだ。 + どれも空を許すのは、書かれていない事件を落とさないため。在所が空でも線は引ける + (時刻は分かっている)。 + */ + const timelineEvents = definition.timeline.flatMap((event) => { + const minutes = minutesOf(event.at) + + if (minutes === undefined) { + return [] + } + + return [ + { + id: event.id, + at: formatClock(minutes), + place: event.location === undefined ? '' : event.location, + room: event.room === undefined ? '' : event.room, + record: event.record === undefined ? '' : event.record, + participants: event.participants.map(characterUuid), + facts: event.facts, + kind: kindOfEvent(event.facts.map((id) => factKinds.get(id))), + }, + ] + }) + /* 時刻軸の両端。timeline から外枠だけを取り出して scenarios 側へ焼く。 真相のテーブルに入れないのは、これがプレイ開始前に見せてよい情報だから @@ -228,6 +288,52 @@ const compileDefinition = ( */ const window = timeWindowOf(definition.timeline) + /** + * 所見の解禁前提を採番し直す。 + * + * DO が持っている発見済みの ID は uuid なので、authoring のローカル ID のまま焼くと、 + * 前提が永久に満たされない所見になる。(所見自身の id は他から参照されないのでそのまま。) + * 遺体と場所で同じ手当てが要るので、一箇所に置く。 + */ + const compileFinding = (finding: ScenarioFinding): ScenarioFinding => ({ + id: finding.id, + statement: finding.statement, + requires: { + revelations: finding.requires.revelations.map(revelationUuid), + evidences: finding.requires.evidences.map(evidenceUuid), + }, + }) + + /* + 場所は二つに割って焼く。名前と紹介と佇まいは調べる前から見えるので公開側へ、 + 所見は調べて初めて出るので真相側へ。遺体とまったく同じ分け方をしている。 + + ID はローカルのまま。場所は `type: location` のソースが指す先で、実行時も + その文字列で突き合わせる(部屋 ID と同じ理由。uuid にすると誰も指せなくなる)。 + */ + const compiledPlaces = definition.places.map((place) => ({ + id: place.id, + name: place.name, + shortName: place.shortName, + introduction: place.introduction, + situation: place.situation, + })) + + const compiledPlaceFindings = definition.places.map((place) => ({ + placeId: place.id, + findings: place.findings.map(compileFinding), + })) + + const victim = definition.victim + + /* + 遺体を調べられる事件かどうかを、ここで公開側へ焼いておく。 + 所見も死因も無いなら調べても何も出ないので、聞き込みの相手に並べない。 + 画面はこの一つだけを見れば決められる——真相のテーブルを覗きに行かずに済む。 + */ + const investigable = + victim !== undefined && (victim.findings.length > 0 || victim.causeOfDeath !== undefined) + return { scenario: { id: scenarioId, @@ -238,8 +344,16 @@ const compileDefinition = ( category: definition.meta.category, timeStart: window === undefined ? null : window.start, timeEnd: window === undefined ? null : window.end, - victimName: definition.victim === undefined ? null : definition.victim.name, - victimIntroduction: definition.victim === undefined ? null : definition.victim.introduction, + victimName: victim === undefined ? null : victim.name, + victimIntroduction: victim === undefined ? null : victim.introduction, + victimFoundAt: victim === undefined || victim.foundAt === undefined ? null : victim.foundAt, + victimFoundIn: victim === undefined || victim.foundIn === undefined ? null : victim.foundIn, + victimEstimatedDeathAt: + victim === undefined || victim.estimatedDeathAt === undefined + ? null + : victim.estimatedDeathAt, + victimInvestigable: investigable, + places: compiledPlaces, isPublished: options.isPublished, difficulty: definition.meta.difficulty, estimatedMinutes: definition.meta.estimatedMinutes, @@ -254,6 +368,11 @@ const compileDefinition = ( method: definition.solution.method, motive: definition.solution.motive, timeline, + timelineEvents, + victimCauseOfDeath: + victim === undefined || victim.causeOfDeath === undefined ? null : victim.causeOfDeath, + victimFindings: victim === undefined ? [] : victim.findings.map(compileFinding), + placeFindings: compiledPlaceFindings, secretKeywords: definition.solution.secretKeywords, }, } diff --git a/db/generate-scenario.ts b/db/generate-scenario.ts index 8fdb0e0..92c36b0 100644 --- a/db/generate-scenario.ts +++ b/db/generate-scenario.ts @@ -97,6 +97,11 @@ if (!result.ok) { throw new Error(`${MAX_ATTEMPTS}回試しましたが、検証を通る定義が得られませんでした。`) } +// 差し戻しきれずに残った指摘。落とすほどではないので、手で直せるよう名指ししておく。 +for (const warning of result.warnings) { + console.log(` 警告: ${warning}`) +} + /* ファイル名はモデルが決めた id をそのまま使う。検証を通った後なので ^[a-z0-9][a-z0-9-]{2,63}$ に収まっており、パスとして解釈される文字は入らない。 diff --git a/db/migrations/0009_alibi-timeline-events.sql b/db/migrations/0009_alibi-timeline-events.sql new file mode 100644 index 0000000..787168a --- /dev/null +++ b/db/migrations/0009_alibi-timeline-events.sql @@ -0,0 +1,2 @@ +ALTER TABLE `evidences` ADD `supports` text DEFAULT '[]' NOT NULL;--> statement-breakpoint +ALTER TABLE `scenario_truths` ADD `timeline_events` text DEFAULT '[]' NOT NULL; \ No newline at end of file diff --git a/db/migrations/0010_victim-findings.sql b/db/migrations/0010_victim-findings.sql new file mode 100644 index 0000000..d8fe7dd --- /dev/null +++ b/db/migrations/0010_victim-findings.sql @@ -0,0 +1,5 @@ +ALTER TABLE `scenario_truths` ADD `victim_cause_of_death` text;--> statement-breakpoint +ALTER TABLE `scenario_truths` ADD `victim_findings` text DEFAULT '[]' NOT NULL;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `victim_found_at` text;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `victim_found_in` text;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `victim_investigable` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/db/migrations/0011_scenario-current-authoring-upgrade.sql b/db/migrations/0011_scenario-current-authoring-upgrade.sql new file mode 100644 index 0000000..9ab4a7f --- /dev/null +++ b/db/migrations/0011_scenario-current-authoring-upgrade.sql @@ -0,0 +1,257 @@ +UPDATE scenarios SET victim_found_at = '21:45', victim_found_in = '支配人室', victim_investigable = 1 WHERE victim_name = '早瀬隆司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'audit-warning', 'at', '21:05', 'place', '山荘内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["hayase-audit-next-morning","natsume-cash-shortage"]'), 'kind', 'claim'), json_object('id', 'fake-message', 'at', '21:16', 'place', 'フロント', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["fake-message-left","no-214-call","paper-from-night-pad"]'), 'kind', 'solid'), json_object('id', 'ask-oda', 'at', '21:24', 'place', '客室棟', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["natsume-asked-oda"]'), 'kind', 'solid'), json_object('id', 'management-corridor', 'at', '21:27', 'place', '管理廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["oda-saw-natsume-office-side"]'), 'kind', 'solid'), json_object('id', 'hayase-death', 'at', '21:30', 'place', '支配人室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["hayase-death-2130","natsume-killed-hayase"]'), 'kind', 'claim'), json_object('id', 'ask-fujino', 'at', '21:31', 'place', '厨房前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '藤野修平' LIMIT 1)), 'facts', json('["natsume-asked-fujino"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:45', 'place', '支配人室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '藤野修平' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["body-found-2145"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"早瀬隆司は支配人室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「翌朝の会計監査メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '夏目か小田に早瀬が翌朝予定していた会計確認について尋ねたら開示する。または遺体・現場を調べ、「翌朝の会計監査メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '翌朝の会計監査メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '資料整理室', victim_investigable = 1 WHERE victim_name = '高瀬静一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'billing-confrontation', 'at', '20:48', 'place', '修道院内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1)), 'facts', json('["repair-overbilling","mizuki-overbilled","takase-confronted-mizuki"]'), 'kind', 'claim'), json_object('id', 'takase-death', 'at', '21:05', 'place', '資料整理室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1)), 'facts', json('["mizuki-killed-takase"]'), 'kind', 'claim'), json_object('id', 'bell-rung', 'at', '21:20', 'place', '鐘楼', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '黒川玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["mizuki-rang-bell-remotely","all-heard-bell","no-one-saw-takase-bell"]'), 'kind', 'solid'), json_object('id', 'test-line-clue', 'at', '21:27', 'place', '聖歌席裏', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["temporary-test-line-exists","genda-thought-line-removed","test-line-tag-remained"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '資料整理室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '黒川玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"高瀬静一は資料整理室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「修復費の追加請求一覧」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '水城に高瀬から追及された修復費について尋ねるか、玄田に問題になっていた工事項目を確認したら開示する。または遺体・現場を調べ、「修復費の追加請求一覧」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '修復費の追加請求一覧'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '書斎', victim_investigable = 1 WHERE victim_name = '塚本誠'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'fraud-discovered', 'at', '21:05', 'place', 'ロッジ内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["payment-fraud","tsukamoto-found-fraud"]'), 'kind', 'claim'), json_object('id', 'summon-note', 'at', '21:10', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1)), 'facts', json('["tsukamoto-summoned-katase"]'), 'kind', 'claim'), json_object('id', 'katase-leaves-game', 'at', '21:12', 'place', '談話室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["katase-left-2112","mamiya-substituted-blue","game-tracks-seats"]'), 'kind', 'solid'), json_object('id', 'tsukamoto-death', 'at', '21:18', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1)), 'facts', json('["katase-killed-tsukamoto"]'), 'kind', 'claim'), json_object('id', 'score-continues', 'at', '21:21', 'place', 'ロッジ内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["blue-score-continued","yoshioka-copied-score-later"]'), 'kind', 'solid'), json_object('id', 'katase-returns', 'at', '21:25', 'place', '談話室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["katase-returned-2125","todo-saw-katase-return"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"塚本誠は書斎で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「外部講師費の精算書」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '片瀬か吉岡に事件前に塚本が確認していた精算書について尋ねたら開示する。または遺体・現場を調べ、「外部講師費の精算書」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '外部講師費の精算書'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:27', victim_found_in = '地図解析室', victim_investigable = 1 WHERE victim_name = '岩代圭吾'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'tag-attached-cart', 'at', '20:58', 'place', '研究所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["locator-tag-removable","kagawa-tag-on-cart"]'), 'kind', 'solid'), json_object('id', 'cart-loop-start', 'at', '21:00', 'place', '測量区画', 'participants', json_array(), 'facts', json('["mapping-cart-auto-loop","tag-track-matches-cart"]'), 'kind', 'solid'), json_object('id', 'kagawa-leaves', 'at', '21:01', 'place', '測量区画', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-left-survey-zone"]'), 'kind', 'claim'), json_object('id', 'tono-sighting', 'at', '21:07', 'place', '連絡通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '遠野澪' LIMIT 1)), 'facts', json('["tono-saw-kagawa-2107"]'), 'kind', 'solid'), json_object('id', 'iwashiro-death', 'at', '21:11', 'place', '地図解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-killed-iwashiro"]'), 'kind', 'claim'), json_object('id', 'cart-loop-end', 'at', '21:15', 'place', '測量区画', 'participants', json_array(), 'facts', json('["mapping-cart-auto-loop","tag-track-matches-cart"]'), 'kind', 'solid'), json_object('id', 'tag-recovered', 'at', '21:17', 'place', '測量区画', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-recovered-tag"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '21:27', 'place', '地図解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '新堂匠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '結城真' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '遠野澪' LIMIT 1)), 'facts', json('["body-found-2127"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"岩代圭吾は地図解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「試料ラベルと測量座標の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '香川か新堂に岩代が事件直前に照合していた試料採取地点について尋ね、測量座標との不一致を追及したら開示する。または遺体・現場を調べ、「試料ラベルと測量座標の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '試料ラベルと測量座標の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '23:10', victim_found_in = '書斎', victim_investigable = 1 WHERE victim_name = '野上修一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'fraud-confrontation', 'at', '21:50', 'place', '山荘内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-forged-expenses","nogami-found-megumi-fraud"]'), 'kind', 'claim'), json_object('id', 'nogami-death', 'at', '22:05', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-killed-nogami"]'), 'kind', 'claim'), json_object('id', 'maki-sees-megumi', 'at', '22:08', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["maki-saw-megumi-study"]'), 'kind', 'solid'), json_object('id', 'megumi-goes-room', 'at', '22:15', 'place', '自室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-originally-asleep-2215"]'), 'kind', 'claim'), json_object('id', 'false-fireplace-sighting', 'at', '22:30', 'place', '暖炉前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1)), 'facts', json('["maki-lied-fireplace","original-only-maki-claimed-sighting"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:10', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '藤村達也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["body-found-2310"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"野上修一は書斎で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「仕入れ帳の水増し」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '倉田に野上と事件直前に揉めた帳簿の内容を追及したら開示する。または遺体・現場を調べ、「仕入れ帳の水増し」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND label = '仕入れ帳の水増し'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '00:22', victim_found_in = '資料室', victim_investigable = 1 WHERE victim_name = '篠宮亮'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'readings-start', 'at', '00:00', 'place', '制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["readings-automatic","signatures-batchable"]'), 'kind', 'solid'), json_object('id', 'sagisawa-leaves', 'at', '00:01', 'place', '制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-left-control"]'), 'kind', 'claim'), json_object('id', 'passage-sighting', 'at', '00:05', 'place', '中央通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '小日向茜' LIMIT 1)), 'facts', json('["kohinata-saw-sagisawa"]'), 'kind', 'solid'), json_object('id', 'shinomiya-death', 'at', '00:08', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-killed-shinomiya"]'), 'kind', 'claim'), json_object('id', 'sagisawa-return', 'at', '00:14', 'place', '制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-returned-control"]'), 'kind', 'claim'), json_object('id', 'batch-sign', 'at', '00:16', 'place', '居住区内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-batch-signed","one-signature-transaction"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '00:22', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鳴海俊' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '小日向茜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '土岐誠' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"篠宮亮は資料室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「部品交換記録と実機番号の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '鷺沢か土岐に篠宮が事件直前に照合していた部品番号と点検表について尋ねたら開示する。または遺体・現場を調べ、「部品交換記録と実機番号の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '部品交換記録と実機番号の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '17:20', victim_found_in = '上部ラウンジ', victim_investigable = 1 WHERE victim_name = '柴田功'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'passenger-leaves', 'at', '16:35', 'place', '船内', 'participants', json_array(), 'facts', json('["passenger-left-before-departure","booked-passengers-42"]'), 'kind', 'solid'), json_object('id', 'shibata-warning', 'at', '16:48', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-skimmed-sales","shibata-found-shortage","shibata-warned-kanda"]'), 'kind', 'claim'), json_object('id', 'secret-interview', 'at', '16:50', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '藤原奈緒' LIMIT 1)), 'facts', json('["fujiwara-secret-interview"]'), 'kind', 'solid'), json_object('id', 'count-sheet-made', 'at', '16:58', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-wrote-count-sheet"]'), 'kind', 'claim'), json_object('id', 'stair-sighting', 'at', '17:03', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '荻原陸' LIMIT 1)), 'facts', json('["ogiwara-saw-kanda-1703"]'), 'kind', 'solid'), json_object('id', 'shibata-death', 'at', '17:06', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-killed-shibata-1706"]'), 'kind', 'claim'), json_object('id', 'kanda-returns', 'at', '17:11', 'place', '下部客室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-returned-lower-1711","count-sheet-says-42"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '17:20', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '荻原陸' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '藤原奈緒' LIMIT 1)), 'facts', json('["body-found-1720"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"柴田功は上部ラウンジで倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「追加券の控えと売上帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '神田に柴田が監査していた帳簿の内容を尋ねるか、藤原に会社の数字で柴田が問題視していた点を尋ねたら開示する。または遺体・現場を調べ、「追加券の控えと売上帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND label = '追加券の控えと売上帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:35', victim_found_in = '祭具倉庫', victim_investigable = 1 WHERE victim_name = '神谷宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kamiya-warning', 'at', '19:50', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["kamiya-warned-tozuka","kamiya-found-kickbacks"]'), 'kind', 'claim'), json_object('id', 'blackout-start', 'at', '20:18', 'place', '会場', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["blackout-started-2018","lighting-preset-ran"]'), 'kind', 'solid'), json_object('id', 'tozuka-leaves', 'at', '20:20', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-left-console-2020"]'), 'kind', 'claim'), json_object('id', 'aihara-sighting', 'at', '20:22', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["aihara-saw-tozuka-2022"]'), 'kind', 'solid'), json_object('id', 'kamiya-death', 'at', '20:23', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-killed-kamiya-2023","spare-key-borrowed"]'), 'kind', 'claim'), json_object('id', 'tozuka-returns', 'at', '20:25', 'place', '操作卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-returned-2025"]'), 'kind', 'claim'), json_object('id', 'lights-return', 'at', '20:26', 'place', '会場', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["lights-restored-2026"]'), 'kind', 'solid'), json_object('id', 'key-check', 'at', '20:28', 'place', '境内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1)), 'facts', json('["makabe-checked-key-2028"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:35', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["body-found-2035"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"神谷宗一は祭具倉庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「照明設備の発注帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '戸塚に設備費の内訳を尋ねるか、真壁に神谷が祭り前から確認していた帳簿について尋ねたら開示する。または遺体・現場を調べ、「照明設備の発注帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND label = '照明設備の発注帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '資料庫', victim_investigable = 1 WHERE victim_name = '今泉孝臣'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'vault-opened', 'at', '21:09', 'place', '希少資料庫', 'participants', json_array(), 'facts', json('["vault-key-only-opens","vault-self-locks","vault-open-2109","vault-stayed-open"]'), 'kind', 'solid'), json_object('id', 'yagami-enters', 'at', '21:14', 'place', '資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '久我遼' LIMIT 1)), 'facts', json('["yagami-entered-vault-2114","kuga-saw-yagami-2114"]'), 'kind', 'solid'), json_object('id', 'imaizumi-enters', 'at', '21:17', 'place', '資料庫', 'participants', json_array(), 'facts', json('["imaizumi-entered-vault-2117"]'), 'kind', 'solid'), json_object('id', 'imaizumi-death', 'at', '21:20', 'place', '希少資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1)), 'facts', json('["yagami-killed-imaizumi"]'), 'kind', 'claim'), json_object('id', 'yagami-exits', 'at', '21:22', 'place', '資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1)), 'facts', json('["yagami-left-vault-2122","vault-closed-2122"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '戸塚誠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '久我遼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '真島香苗' LIMIT 1)), 'facts', json('["key-found-on-imaizumi","body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"今泉孝臣は資料庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「修復処置記録と資料状態の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '八神か久我に今泉が事件直前に調べていた修復記録について尋ね、実資料との不一致を追及したら開示する。または遺体・現場を調べ、「修復処置記録と資料状態の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '修復処置記録と資料状態の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:35', victim_found_in = '保存記録室', victim_investigable = 1 WHERE victim_name = '磯崎章'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'door-opened-for-maintenance', 'at', '19:50', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1)), 'facts', json('["door-held-open-1950","door-remained-open-2022","no-new-unlock"]'), 'kind', 'solid'), json_object('id', 'mitamura-warning', 'at', '20:00', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["mitamura-told-yabuki-open"]'), 'kind', 'solid'), json_object('id', 'yabuki-enters', 'at', '20:09', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["yabuki-entered-unlogged"]'), 'kind', 'claim'), json_object('id', 'isozaki-death', 'at', '20:12', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["isozaki-death-2012","yabuki-killed-isozaki"]'), 'kind', 'claim'), json_object('id', 'corridor-return', 'at', '20:16', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '木瀬亮' LIMIT 1)), 'facts', json('["kise-saw-yabuki-return-2016"]'), 'kind', 'solid'), json_object('id', 'door-closes', 'at', '20:22', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1)), 'facts', json('["door-remained-open-2022"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:35', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '木瀬亮' LIMIT 1)), 'facts', json('["body-found-2035"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"磯崎章は保存記録室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「監督部署への報告草案」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '矢吹か三田村に磯崎が翌朝提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「監督部署への報告草案」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '監督部署への報告草案'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '18:55', victim_found_in = '収蔵庫前', victim_investigable = 1 WHERE victim_name = '鳥羽薫'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'forgery-discovered', 'at', '18:25', 'place', '美術館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["toba-found-forgery"]'), 'kind', 'claim'), json_object('id', 'sakaki-confronted', 'at', '18:29', 'place', '美術館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["toba-confronted-sakaki"]'), 'kind', 'claim'), json_object('id', 'mido-bribe', 'at', '18:32', 'place', '美術館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '御堂和也' LIMIT 1)), 'facts', json('["mido-offered-bribe"]'), 'kind', 'claim'), json_object('id', 'uv-test-start', 'at', '18:35', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["uv-test-started"]'), 'kind', 'solid'), json_object('id', 'sakaki-leaves', 'at', '18:38', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["uv-lamp-off-1837","sakaki-left-restoration"]'), 'kind', 'solid'), json_object('id', 'enomoto-sighting', 'at', '18:44', 'place', '収蔵庫前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榎本駿' LIMIT 1)), 'facts', json('["enomoto-saw-sakaki-1844"]'), 'kind', 'solid'), json_object('id', 'toba-death', 'at', '18:46', 'place', '収蔵庫前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["sakaki-killed-toba-1846"]'), 'kind', 'claim'), json_object('id', 'sakaki-returns', 'at', '18:50', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["sakaki-returned-1850","uv-test-resumed"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '18:55', 'place', '収蔵庫前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '御堂和也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榎本駿' LIMIT 1)), 'facts', json('["body-found-1855"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"鳥羽薫は収蔵庫前で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「鳥羽の額装検査メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '榊か御堂に鳥羽が事件前に作品の真贋を調べていなかったか尋ね、額装の違いへ話が及んだら開示する。または遺体・現場を調べ、「鳥羽の額装検査メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND label = '鳥羽の額装検査メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '06:28', victim_found_in = '種子保管区', victim_investigable = 1 WHERE victim_name = 'ミラ・ヴォス'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'mira-confronts-sera', 'at', '05:45', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["sera-seed-diversion","mira-found-diversion","mira-would-audit"]'), 'kind', 'claim'), json_object('id', 'dario-sees-sera', 'at', '05:52', 'place', '種子保管区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ダリオ・ケイン' LIMIT 1)), 'facts', json('["dario-saw-sera-0552"]'), 'kind', 'solid'), json_object('id', 'mira-death', 'at', '05:57', 'place', '種子保管区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["sera-killed-mira"]'), 'kind', 'claim'), json_object('id', 'agriculture-six', 'at', '06:00', 'place', '農業区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["agriculture-dawn-0600-local","sera-claimed-after-six"]'), 'kind', 'solid'), json_object('id', 'medical-six', 'at', '06:20', 'place', '医療区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ユナ・パク' LIMIT 1)), 'facts', json('["medical-dawn-0600-local"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '06:28', 'place', '種子保管区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ダリオ・ケイン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ユナ・パク' LIMIT 1)), 'facts', json('["body-found-0628"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"ミラ・ヴォスは種子保管区で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「希少種子の監査記録」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'セラかダリオにミラが事件直前に確認していた種子在庫を尋ねたら開示する。または遺体・現場を調べ、「希少種子の監査記録」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND label = '希少種子の監査記録'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '06:30', victim_found_in = '標本庫', victim_investigable = 1 WHERE victim_name = '木島祥子'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'confrontation', 'at', '05:45', 'place', '植物園内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["kijima-found-missing-tags","kijima-confronted-narahara"]'), 'kind', 'claim'), json_object('id', 'narahara-leaves', 'at', '05:52', 'place', '東温室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-left-east-house","manual-water-zero"]'), 'kind', 'solid'), json_object('id', 'auto-misting', 'at', '06:00', 'place', '東温室', 'participants', json_array(), 'facts', json('["auto-misting-ran-0600"]'), 'kind', 'solid'), json_object('id', 'corridor-sighting', 'at', '06:05', 'place', '標本庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '大西蒼太' LIMIT 1)), 'facts', json('["onishi-saw-narahara-0605"]'), 'kind', 'solid'), json_object('id', 'kijima-death', 'at', '06:08', 'place', '標本庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-killed-kijima-0608"]'), 'kind', 'claim'), json_object('id', 'narahara-returns', 'at', '06:15', 'place', '東温室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-returned-east-0615"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '06:30', 'place', '標本庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '水沢浩司' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '大西蒼太' LIMIT 1)), 'facts', json('["body-found-0630"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"木島祥子は標本庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「木島の希少植物管理ノート」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '楢原に希少植物の管理札が足りない理由を尋ねるか、大西に木島が前日から調べていた記録を尋ねたら開示する。または遺体・現場を調べ、「木島の希少植物管理ノート」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND label = '木島の希少植物管理ノート'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:00', victim_found_in = '編集長室', victim_investigable = 1 WHERE victim_name = '石橋礼司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'royalty-confrontation', 'at', '20:25', 'place', '編集部内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-diverted-royalties","ishibashi-found-royalty-gap","ishibashi-warned-kawase"]'), 'kind', 'claim'), json_object('id', 'shido-argument', 'at', '20:31', 'place', '編集部内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '志堂透' LIMIT 1)), 'facts', json('["shido-argued-2031","shido-plagiarism"]'), 'kind', 'solid'), json_object('id', 'kawase-enters', 'at', '20:38', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-entered-2038"]'), 'kind', 'claim'), json_object('id', 'ishibashi-death', 'at', '20:41', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-killed-ishibashi-2041"]'), 'kind', 'claim'), json_object('id', 'kawase-leaves', 'at', '20:44', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '藤本圭' LIMIT 1)), 'facts', json('["fujimoto-saw-kawase-2044"]'), 'kind', 'solid'), json_object('id', 'contract-print', 'at', '20:47', 'place', '編集部', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["contract-printed-2047","kawase-forged-signature"]'), 'kind', 'solid'), json_object('id', 'claimed-signing', 'at', '20:50', 'place', '編集部内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["signed-contract-claims-2050"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '21:00', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '藤本圭' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '志堂透' LIMIT 1)), 'facts', json('["body-found-2100"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"石橋礼司は編集長室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「海外版権の送金記録と支払帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '川瀬に石橋が調べていた版権収入について尋ねるか、藤本に石橋が事件前に確認していた帳簿を尋ねたら開示する。または遺体・現場を調べ、「海外版権の送金記録と支払帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND label = '海外版権の送金記録と支払帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:42', victim_found_in = '修復室', victim_investigable = 1 WHERE victim_name = '倉橋宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'clock-offset-remains', 'at', '19:36', 'place', '館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '城戸篤' LIMIT 1)), 'facts', json('["master-clock-fast-eleven","gallery-clocks-follow-master","kido-caused-clock-offset"]'), 'kind', 'solid'), json_object('id', 'false-half-past-chime', 'at', '20:19', 'place', '館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '保科悠人' LIMIT 1)), 'facts', json('["half-past-chime-actual-2019","security-clock-accurate"]'), 'kind', 'solid'), json_object('id', 'shiba-kurahashi-talk', 'at', '20:20', 'place', '西回廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-spoke-kurahashi-2020"]'), 'kind', 'solid'), json_object('id', 'shiba-leaves-west', 'at', '20:22', 'place', '西回廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-left-west-corridor"]'), 'kind', 'claim'), json_object('id', 'asakura-sighting', 'at', '20:24', 'place', '北廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1)), 'facts', json('["asakura-saw-shiba-2024"]'), 'kind', 'solid'), json_object('id', 'kurahashi-death', 'at', '20:28', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-killed-kurahashi"]'), 'kind', 'claim'), json_object('id', 'shiba-return', 'at', '20:34', 'place', '展示準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-returned-gallery"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '20:42', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '保科悠人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '城戸篤' LIMIT 1)), 'facts', json('["body-found-2042"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"倉橋宗一は修復室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「倉橋の来歴照合メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '志波か保科に倉橋が事件直前に調べていた展示時計の来歴について尋ね、登録内容の不一致を追及したら開示する。または遺体・現場を調べ、「倉橋の来歴照合メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '倉橋の来歴照合メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:00', victim_found_in = '執務室', victim_investigable = 1 WHERE victim_name = '長峰宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'invoice-confrontation', 'at', '20:52', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["renovation-shortage","ayase-falsified-invoices","nagamine-called-ayase"]'), 'kind', 'claim'), json_object('id', 'nagamine-death', 'at', '21:05', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["ayase-killed-nagamine"]'), 'kind', 'claim'), json_object('id', 'coat-missing', 'at', '21:18', 'place', 'フロント裏', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '星野結' LIMIT 1)), 'facts', json('["hoshino-saw-empty-coat-hook"]'), 'kind', 'solid'), json_object('id', 'silhouette-staged', 'at', '21:23', 'place', 'ホテル内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["ayase-staged-silhouette","desk-lamp-left-on","stand-feet-dust-mark"]'), 'kind', 'solid'), json_object('id', 'silhouette-seen', 'at', '21:30', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '神田理一' LIMIT 1)), 'facts', json('["kanda-saw-silhouette","kanda-did-not-hear-voice"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:00', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '星野結' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '神田理一' LIMIT 1)), 'facts', json('["body-found-2200","coat-returned-after-discovery"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"長峰宗一は執務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「水増しされた改装請求書」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '綾瀬に改装費の処理を尋ねるか、長峰が直前まで確認していた書類について追及したら開示する。または遺体・現場を調べ、「水増しされた改装請求書」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '水増しされた改装請求書'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:25', victim_found_in = '駅務室', victim_investigable = 1 WHERE victim_name = '藤崎正雄'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'refund-confrontation', 'at', '21:55', 'place', '駅構内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["arima-skimmed-refunds","fujisaki-found-refund-gap","fujisaki-warned-arima"]'), 'kind', 'claim'), json_object('id', 'arima-office-corridor', 'at', '22:05', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1)), 'facts', json('["arima-entered-office-2205","sawada-saw-arima-2205"]'), 'kind', 'solid'), json_object('id', 'fujisaki-death', 'at', '22:08', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["arima-killed-fujisaki-2208"]'), 'kind', 'claim'), json_object('id', 'arima-returns-platform', 'at', '22:12', 'place', 'ホーム', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1)), 'facts', json('["arima-left-office-2212","komori-saw-arima-2212"]'), 'kind', 'solid'), json_object('id', 'last-train-departs', 'at', '22:18', 'place', '駅構内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1)), 'facts', json('["last-train-delayed"]'), 'kind', 'solid'), json_object('id', 'certificates-print', 'at', '22:20', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["certificates-printed-2220"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:25', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1)), 'facts', json('["body-found-2225"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"藤崎正雄は駅務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「券売機返金処理と現金残高の差」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '有馬に藤崎が事件前に確認していた返金処理について尋ねるか、小森に駅長が帳簿を調べていた理由を尋ねたら開示する。または遺体・現場を調べ、「券売機返金処理と現金残高の差」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '券売機返金処理と現金残高の差'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:50', victim_found_in = '解析室', victim_investigable = 1 WHERE victim_name = 'エレナ・ヴァルガ'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'noah-question', 'at', '21:18', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1)), 'facts', json('["noah-sent-question-2118"]'), 'kind', 'solid'), json_object('id', 'fraud-confrontation', 'at', '21:25', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1)), 'facts', json('["noah-budget-fraud","elena-found-fraud","elena-would-report"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '21:33', 'place', '連絡廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ルイス・オルテガ' LIMIT 1)), 'facts', json('["luis-saw-noah-corridor"]'), 'kind', 'solid'), json_object('id', 'elena-death', 'at', '21:36', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1)), 'facts', json('["noah-killed-elena"]'), 'kind', 'claim'), json_object('id', 'delayed-reply', 'at', '21:38', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1)), 'facts', json('["earth-reply-arrived-2138","noah-played-arrival-tone","earth-mars-delay"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:50', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ルイス・オルテガ' LIMIT 1)), 'facts', json('["body-found-2150"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"エレナ・ヴァルガは解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「調達予算の監査ファイル」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'ノアかアミラにエレナが翌朝送ろうとしていた監査資料について尋ねたら開示する。または遺体・現場を調べ、「調達予算の監査ファイル」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND label = '調達予算の監査ファイル'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '00:12', victim_found_in = '第2ブース', victim_investigable = 1 WHERE victim_name = '大門修一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'opening-recording', 'at', '23:20', 'place', '局内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1)), 'facts', json('["opening-recorded-2320"]'), 'kind', 'claim'), json_object('id', 'natsume-meets-daimon', 'at', '23:38', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["natsume-secret-meeting"]'), 'kind', 'solid'), json_object('id', 'natsume-leaves', 'at', '23:43', 'place', 'ロビー', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["natsume-left-2343"]'), 'kind', 'solid'), json_object('id', 'minobe-enters', 'at', '23:46', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["minobe-entered-2346"]'), 'kind', 'claim'), json_object('id', 'daimon-death', 'at', '23:48', 'place', 'ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["daimon-died-2348"]'), 'kind', 'claim'), json_object('id', 'minobe-returns', 'at', '23:50', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1)), 'facts', json('["kuze-saw-minobe-2350"]'), 'kind', 'solid'), json_object('id', 'recording-queued', 'at', '23:53', 'place', '送出室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["minobe-queued-recording"]'), 'kind', 'claim'), json_object('id', 'opening-airs', 'at', '00:00', 'place', '局内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["recorded-opening-aired"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '00:12', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["body-found-0012"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"大門修一は第2ブースで倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「大門が保存したスポンサー報告の比較メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '美濃部に制作費やスポンサー報告の不一致について尋ねるか、夏目に大門が最近スポンサー実績を調べていなかったか尋ねたら開示する。または遺体・現場を調べ、「大門が保存したスポンサー報告の比較メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND label = '大門が保存したスポンサー報告の比較メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '23:10', victim_found_in = '解析室', victim_investigable = 1 WHERE victim_name = '牧瀬航'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'sync-failure', 'at', '21:50', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '樋口海' LIMIT 1)), 'facts', json('["central-clock-offset","higuchi-hid-sync-failure"]'), 'kind', 'solid'), json_object('id', 'false-alibi-window', 'at', '22:13', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1)), 'facts', json('["terminal-uses-central-clock","access-uses-central-clock","observation-log-central-clock","wall-clock-synced","four-times-not-independent"]'), 'kind', 'solid'), json_object('id', 'real-corridor-sighting', 'at', '22:20', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '沢渡直人' LIMIT 1)), 'facts', json('["handheld-clock-correct","sawatari-saw-shinomiya-real-2220","displayed-time-was-2227"]'), 'kind', 'solid'), json_object('id', 'makise-death', 'at', '22:23', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1)), 'facts', json('["makise-death-real-2223","shinomiya-killed-makise"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:10', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '樋口海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '沢渡直人' LIMIT 1)), 'facts', json('["body-found-2310"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"牧瀬航は解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「翌朝の研究会議議題」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '篠宮か樋口に翌朝の研究会議で牧瀬が扱う予定だった議題を尋ねたら開示する。または遺体・現場を調べ、「翌朝の研究会議議題」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '翌朝の研究会議議題'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '事務室', victim_investigable = 1 WHERE victim_name = '星名悟'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'badge-loan', 'at', '20:55', 'place', '診療所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1)), 'facts', json('["kuga-borrowed-badge","sera-approved-loan"]'), 'kind', 'claim'), json_object('id', 'hoshina-death', 'at', '21:08', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1)), 'facts', json('["hoshina-death-2108","sera-killed-hoshina"]'), 'kind', 'claim'), json_object('id', 'orange-passage', 'at', '21:18', 'place', '診療所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '相原誠' LIMIT 1)), 'facts', json('["orange-badge-passed-2118","aihara-saw-orange-suit","person-was-kuga-2118"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '相原誠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"星名悟は事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「本部報告予定の確認メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '世良か久我に星名が翌朝予定していた本部報告の内容を尋ねたら開示する。または遺体・現場を調べ、「本部報告予定の確認メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '本部報告予定の確認メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '19:15', victim_found_in = '店奥', victim_investigable = 1 WHERE victim_name = '水野英治'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kuroda-offer', 'at', '18:28', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1)), 'facts', json('["kuroda-secret-offer"]'), 'kind', 'claim'), json_object('id', 'swap-discovered', 'at', '18:37', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["mizuno-discovered-swap"]'), 'kind', 'claim'), json_object('id', 'kuroda-leaves', 'at', '18:42', 'place', '軒下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1)), 'facts', json('["kuroda-left-1842"]'), 'kind', 'solid'), json_object('id', 'kuroda-sighting', 'at', '18:47', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["kuroda-saw-makino-1847"]'), 'kind', 'solid'), json_object('id', 'mizuno-death', 'at', '18:50', 'place', '店奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["makino-killed-mizuno-1850"]'), 'kind', 'claim'), json_object('id', 'makino-departs', 'at', '18:56', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '瀬名真琴' LIMIT 1)), 'facts', json('["makino-left-1856","sena-saw-makino-leave"]'), 'kind', 'solid'), json_object('id', 'parcel-posted', 'at', '19:08', 'place', '郵便窓口', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["post-receipt-1908"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '19:15', 'place', '店奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '瀬名真琴' LIMIT 1)), 'facts', json('["body-found-1915"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"水野英治は店奥で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「店頭に残った複製本」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '牧野か黒田に事件当日の高価な初版本について詳しく尋ね、真贋に疑問が出たら開示する。または遺体・現場を調べ、「店頭に残った複製本」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '店頭に残った複製本'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:20', victim_found_in = '事務室', victim_investigable = 1 WHERE victim_name = '周文海'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'blank-form-signed', 'at', '21:35', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1)), 'facts', json('["zhou-signed-blank-form"]'), 'kind', 'claim'), json_object('id', 'chen-sees-form', 'at', '21:40', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1)), 'facts', json('["chen-saw-blank-time"]'), 'kind', 'solid'), json_object('id', 'zhou-death', 'at', '21:52', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1)), 'facts', json('["lin-killed-zhou","lin-smuggled-silk","zhou-found-smuggling"]'), 'kind', 'claim'), json_object('id', 'time-added', 'at', '22:05', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1)), 'facts', json('["lin-added-2205","top-sheet-time-added-later"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:20', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '王世傑' LIMIT 1)), 'facts', json('["body-found-2220"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"周文海は事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「絹荷の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '林か陳に周が事件前に調べていた荷の不一致を尋ねたら開示する。または遺体・現場を調べ、「絹荷の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND label = '絹荷の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:50', victim_found_in = '館内', victim_investigable = 1 WHERE victim_name = '青沼卓'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'snowfall-begins', 'at', '20:10', 'place', '母屋', 'participants', json_array(), 'facts', json('["snow-started-2010"]'), 'kind', 'solid'), json_object('id', 'kurata-crosses', 'at', '20:17', 'place', '離れ展示室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1)), 'facts', json('["kurata-crossed-2017"]'), 'kind', 'claim'), json_object('id', 'aoonuma-death', 'at', '20:20', 'place', '離れ展示室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1)), 'facts', json('["aoonuma-death-2020","kurata-killed-aoonuma"]'), 'kind', 'claim'), json_object('id', 'path-cleared', 'at', '20:26', 'place', '屋外通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["anzai-cleared-path-2026","snow-covered-after-clearing"]'), 'kind', 'solid'), json_object('id', 'kurata-back-main', 'at', '20:31', 'place', '母屋裏口', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["anzai-saw-kurata-2031"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:50', 'place', '館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '江波涼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["body-found-2050","no-tracks-at-discovery"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"青沼卓は館内で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「来歴記録の修正履歴」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '倉田か江波に青沼が直前まで確認していた作品来歴について尋ねたら開示する。または遺体・現場を調べ、「来歴記録の修正履歴」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '来歴記録の修正履歴'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:15', victim_found_in = '資料室', victim_investigable = 1 WHERE victim_name = '神崎遼'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kanzaki-warning', 'at', '21:40', 'place', '観測所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["kanzaki-found-fabrication","kanzaki-would-retract"]'), 'kind', 'claim'), json_object('id', 'kurose-data-room', 'at', '21:48', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '黒瀬俊介' LIMIT 1)), 'facts', json('["kurose-entered-data-room","kurose-copied-data"]'), 'kind', 'solid'), json_object('id', 'interval-start', 'at', '21:50', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["camera-interval-started","camera-kept-shooting"]'), 'kind', 'solid'), json_object('id', 'hiyama-leaves-roof', 'at', '21:54', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-left-roof-2154"]'), 'kind', 'claim'), json_object('id', 'muroi-sighting', 'at', '21:57', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '室井邦彦' LIMIT 1)), 'facts', json('["muroi-saw-hiyama-2157"]'), 'kind', 'solid'), json_object('id', 'kanzaki-death', 'at', '22:00', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-killed-kanzaki-2200"]'), 'kind', 'claim'), json_object('id', 'hiyama-returns', 'at', '22:06', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-returned-roof-2206"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '22:15', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '黒瀬俊介' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '室井邦彦' LIMIT 1)), 'facts', json('["body-found-2215"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"神崎遼は資料室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「神崎の観測データ検証メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '日山か黒瀬に神崎が直前まで調べていたデータについて尋ね、不正補正の可能性を追及したら開示する。または遺体・現場を調べ、「神崎の観測データ検証メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND label = '神崎の観測データ検証メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '2026-01-16T07:10:00+09:00', victim_found_in = '事務室', victim_investigable = 1 WHERE victim_name = '佐久間隆志'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'shortage-discovered', 'at', '21:35', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1)), 'facts', json('["audit-shortage-found"]'), 'kind', 'claim'), json_object('id', 'confrontation', 'at', '21:50', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["sakuma-confronted-fuyuki","fuyuki-diverted-sales"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '22:06', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '久世圭太' LIMIT 1)), 'facts', json('["kuze-saw-fuyuki-office-side"]'), 'kind', 'solid'), json_object('id', 'sakuma-death', 'at', '22:10', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["fuyuki-killed-sakuma","fuyuki-left-victim-coat"]'), 'kind', 'claim'), json_object('id', 'staged-chores', 'at', '05:35', 'place', '農園内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["fuyuki-did-morning-chores","feed-board-magnet-moved","fuyuki-boots-wet-straw"]'), 'kind', 'solid'), json_object('id', 'kitchen-light-seen', 'at', '05:48', 'place', '農園内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1)), 'facts', json('["kitchen-light-on","nozomi-assumed-uncle-awake"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '07:10', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '久世圭太' LIMIT 1)), 'facts', json('["body-found-0710"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"佐久間隆志は事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「出荷伝票の不足メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '希か冬木に前夜の帳簿確認と不足伝票について尋ね、経理上の問題を追及したら開示する。または遺体・現場を調べ、「出荷伝票の不足メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '出荷伝票の不足メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:05', victim_found_in = '保存庫', victim_investigable = 1 WHERE victim_name = '荻原直哉'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'report-confrontation', 'at', '21:02', 'place', '額装作業室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["forged-restoration-report","ogiwara-found-forgery","ogiwara-called-kosaka"]'), 'kind', 'claim'), json_object('id', 'ogiwara-death', 'at', '21:10', 'place', '額装作業室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-killed-ogiwara-framing","framing-paper-fibers"]'), 'kind', 'solid'), json_object('id', 'transfer-to-vault', 'at', '21:18', 'place', '保存庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-moved-ogiwara","cart-used-after-cleaning"]'), 'kind', 'solid'), json_object('id', 'kosaka-exits-vault', 'at', '21:23', 'place', '搬送廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-left-vault-before-lock","kosaka-apron-dust"]'), 'kind', 'solid'), json_object('id', 'vault-seals', 'at', '21:30', 'place', '保存庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '多田圭' LIMIT 1)), 'facts', json('["vault-night-mode","framing-room-open-before-2130","vault-closed-2130"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:05', 'place', '保存庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '朝倉凪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '多田圭' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["body-found-2205"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"荻原直哉は保存庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「修復工程と材料在庫の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '香坂に事件前に荻原から指摘された修復報告を尋ねるか、朝倉に材料在庫の不一致を確認したら開示する。または遺体・現場を調べ、「修復工程と材料在庫の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '修復工程と材料在庫の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '23:18', victim_found_in = '編集室', victim_investigable = 1 WHERE victim_name = '冬木圭介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'roomtone-loop-made', 'at', '22:46', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-made-roomtone-loop"]'), 'kind', 'solid'), json_object('id', 'loop-recording-starts', 'at', '22:50', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["loop-routed-to-recorder","recording-repeats-identically"]'), 'kind', 'solid'), json_object('id', 'manabe-leaves', 'at', '22:53', 'place', '監視席', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-left-booth"]'), 'kind', 'claim'), json_object('id', 'shirase-hearing', 'at', '22:56', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '白瀬環' LIMIT 1)), 'facts', json('["shirase-heard-loop"]'), 'kind', 'solid'), json_object('id', 'makimura-sighting', 'at', '22:58', 'place', '機材廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '牧村葉月' LIMIT 1)), 'facts', json('["makimura-saw-manabe"]'), 'kind', 'solid'), json_object('id', 'fuyuki-death', 'at', '23:02', 'place', '編集室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-killed-fuyuki"]'), 'kind', 'claim'), json_object('id', 'manabe-return', 'at', '23:08', 'place', '監視席', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-returned-booth"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:18', 'place', '編集室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '鷹野徹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '白瀬環' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '牧村葉月' LIMIT 1)), 'facts', json('["body-found-2318"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"冬木圭介は編集室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「未公開音源の複製履歴」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '真鍋か鷹野に冬木が事件前に調べていた音源データの持ち出しについて尋ね、複製履歴を追及したら開示する。または遺体・現場を調べ、「未公開音源の複製履歴」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '未公開音源の複製履歴'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:36', victim_found_in = '演出控室', victim_investigable = 1 WHERE victim_name = '瀬尾雅人'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'auto-cues-set', 'at', '21:57', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["cue-console-auto-mode","kunieda-set-auto-cues"]'), 'kind', 'solid'), json_object('id', 'cues-start', 'at', '22:00', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1)), 'facts', json('["cues-ran-automatically","toba-saw-light-changes"]'), 'kind', 'solid'), json_object('id', 'kunieda-leaves', 'at', '22:03', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-left-console"]'), 'kind', 'claim'), json_object('id', 'sasai-sighting', 'at', '22:10', 'place', '演出控室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '笹井徹' LIMIT 1)), 'facts', json('["sasai-saw-kunieda-2210"]'), 'kind', 'solid'), json_object('id', 'seo-death', 'at', '22:14', 'place', '演出控室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-killed-seo"]'), 'kind', 'claim'), json_object('id', 'kunieda-return', 'at', '22:19', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-returned-console"]'), 'kind', 'claim'), json_object('id', 'cues-end', 'at', '22:20', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1)), 'facts', json('["cues-ran-automatically"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:36', 'place', '演出控室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '柊真琴' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '笹井徹' LIMIT 1)), 'facts', json('["body-found-2236"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"瀬尾雅人は演出控室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「架空スタッフを含む残業費一覧」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '国枝か柊に瀬尾が事件前に問題視していた残業費請求について尋ね、スタッフ名の実在性を追及したら開示する。または遺体・現場を調べ、「架空スタッフを含む残業費一覧」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '架空スタッフを含む残業費一覧'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '00:12', victim_found_in = '検疫準備室', victim_investigable = 1 WHERE victim_name = '江波慎吾'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'feeder-set', 'at', '23:51', 'place', '給餌台', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["feeder-auto-capable","feeding-lamp-motor-linked","morishita-set-auto-feeder"]'), 'kind', 'solid'), json_object('id', 'first-cycle', 'at', '23:55', 'place', '展示側', 'participants', json_array(), 'facts', json('["feeder-cycled"]'), 'kind', 'solid'), json_object('id', 'morishita-leaves', 'at', '23:57', 'place', '給餌台', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-left-station"]'), 'kind', 'claim'), json_object('id', 'sagara-sighting', 'at', '00:02', 'place', '検疫準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '相良芳江' LIMIT 1)), 'facts', json('["sagara-saw-morishita"]'), 'kind', 'solid'), json_object('id', 'enami-death', 'at', '00:05', 'place', '検疫準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-killed-enami"]'), 'kind', 'claim'), json_object('id', 'morishita-return', 'at', '00:09', 'place', '給餌台', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-returned"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '00:12', 'place', '検疫準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '御子柴徹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '榊原直' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '相良芳江' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"江波慎吾は検疫準備室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「飼育記録の版差分」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '森下か榊原に江波が事件直前に調べていた飼育記録について尋ね、改ざんの可能性を追及したら開示する。または遺体・現場を調べ、「飼育記録の版差分」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '飼育記録の版差分'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:10', victim_found_in = '旧制御室', victim_investigable = 1 WHERE victim_name = '峰岸達也'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'records-confrontation', 'at', '20:12', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-falsified-inspection","minegishi-found-falsification","minegishi-called-kido"]'), 'kind', 'claim'), json_object('id', 'walkway-still-open', 'at', '20:15', 'place', '保守歩廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["walkway-passable-2015","old-walkway-connects"]'), 'kind', 'solid'), json_object('id', 'kido-enters', 'at', '20:18', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-used-walkway"]'), 'kind', 'claim'), json_object('id', 'minegishi-death', 'at', '20:24', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-killed-minegishi"]'), 'kind', 'claim'), json_object('id', 'kido-leaves', 'at', '20:29', 'place', '保守歩廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-left-before-rise","kido-boot-silt"]'), 'kind', 'solid'), json_object('id', 'water-cuts-route', 'at', '20:40', 'place', '保守歩廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["walkway-closed-2040","water-log-recorded-rise"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:10', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '西園寺悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["front-door-remained-locked","body-found-2110"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"峰岸達也は旧制御室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「未実施箇所の点検票」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '城戸か西園寺に事件当日の監査対象となった点検票について尋ねたら開示する。または遺体・現場を調べ、「未実施箇所の点検票」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '未実施箇所の点検票'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:02', victim_found_in = '執務室', victim_investigable = 1 WHERE victim_name = '榊原宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'mail-drafted', 'at', '18:11', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["mail-drafted-1811"]'), 'kind', 'solid'), json_object('id', 'mail-scheduled', 'at', '18:12', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["mail-scheduled-2142","aizawa-knew-scheduled-mail"]'), 'kind', 'solid'), json_object('id', 'aizawa-enters-office', 'at', '21:24', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["aizawa-entered-office-2124"]'), 'kind', 'claim'), json_object('id', 'sakakibara-death', 'at', '21:28', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["aizawa-killed-sakakibara"]'), 'kind', 'claim'), json_object('id', 'horie-sighting', 'at', '21:29', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '堀江充' LIMIT 1)), 'facts', json('["horie-saw-aizawa-2129"]'), 'kind', 'solid'), json_object('id', 'aizawa-joins-hatori', 'at', '21:35', 'place', '講義室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '羽鳥栞' LIMIT 1)), 'facts', json('["aizawa-joined-hatori-2135","aizawa-with-hatori-until-2155"]'), 'kind', 'solid'), json_object('id', 'scheduled-mail-sends', 'at', '21:42', 'place', '執務室', 'participants', json_array(), 'facts', json('["mail-sent-automatically"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:02', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '御影崇' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '羽鳥栞' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '堀江充' LIMIT 1)), 'facts', json('["body-found-2202"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"榊原宗一は執務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「相沢の小口支出重複一覧」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '相沢か御影に榊原が夕方確認していた支出一覧を尋ね、重複と翌朝の監査予定を追及したら開示する。または遺体・現場を調べ、「相沢の小口支出重複一覧」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '相沢の小口支出重複一覧'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:50', victim_found_in = '西外扉', victim_investigable = 1 WHERE victim_name = '倉橋徹'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'west-seal', 'at', '18:40', 'place', '西外扉', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透' LIMIT 1)), 'facts', json('["west-gate-sealed"]'), 'kind', 'solid'), json_object('id', 'east-round', 'at', '19:25', 'place', '東側', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1)), 'facts', json('["torigoe-coat-wet-before"]'), 'kind', 'claim'), json_object('id', 'panel-reading', 'at', '20:12', 'place', '制御盤', 'participants', json_array(), 'facts', json('["slate-entry-2012","slate-written-after-round","torigoe-copied-panel-reading"]'), 'kind', 'solid'), json_object('id', 'stair-sighting', 'at', '20:21', 'place', '内階段', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '橋場圭' LIMIT 1)), 'facts', json('["hashiba-saw-torigoe-2021"]'), 'kind', 'solid'), json_object('id', 'kurahashi-death', 'at', '20:24', 'place', '灯台内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1)), 'facts', json('["kurahashi-death-2024","torigoe-killed-kurahashi"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '20:50', 'place', '西外扉', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '橋場圭' LIMIT 1)), 'facts', json('["body-found-2050","west-seal-intact"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"倉橋徹は西外扉で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「補給報告の在庫差メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '鳥越か森下に翌日の補給報告と燃料在庫の差について尋ねたら開示する。または遺体・現場を調べ、「補給報告の在庫差メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '補給報告の在庫差メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:50', victim_found_in = '帳場奥', victim_investigable = 1 WHERE victim_name = '桐谷宗介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'supply-confrontation', 'at', '20:58', 'place', '山宿内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["supplier-kickback","kiritani-found-kickback","kiritani-confronted-akiwa"]'), 'kind', 'claim'), json_object('id', 'kiritani-death', 'at', '21:08', 'place', '帳場奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["akiwa-killed-kiritani","akiwa-took-cane"]'), 'kind', 'claim'), json_object('id', 'tapping-staged', 'at', '21:25', 'place', '旧階段', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '森崎透' LIMIT 1)), 'facts', json('["akiwa-made-tapping","morisaki-heard-taps","morisaki-heard-no-steps"]'), 'kind', 'solid'), json_object('id', 'cane-hidden', 'at', '21:31', 'place', '厨房脇', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["cane-found-kitchen","rail-fresh-marks"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:50', 'place', '帳場奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '榊原蓮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '森崎透' LIMIT 1)), 'facts', json('["body-found-2150"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"桐谷宗介は帳場奥で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「不自然な仕入れ伝票」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '秋庭に仕入れ業者との関係を尋ねるか、桐谷が事件前に確認していた伝票について追及したら開示する。または遺体・現場を調べ、「不自然な仕入れ伝票」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '不自然な仕入れ伝票'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:00', victim_found_in = '投影準備室', victim_investigable = 1 WHERE victim_name = '犬塚誠'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'inuzuka-death', 'at', '21:24', 'place', '投影準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1)), 'facts', json('["inuzuka-death-2124","onodera-killed-inuzuka"]'), 'kind', 'claim'), json_object('id', 'booth-lights-down', 'at', '21:30', 'place', '投影室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["booth-dark-2130","glass-reflects-dark-booth"]'), 'kind', 'solid'), json_object('id', 'reflected-sighting', 'at', '21:35', 'place', '天象館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["onodera-behind-matsuda-2135","matsuda-saw-white-figure","figure-was-reflection","onodera-endorsed-sighting"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:00', 'place', '投影準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '朝倉葉月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["body-found-2200"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"犬塚誠は投影準備室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「運営法人への予算報告草案」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '小野寺か朝倉に犬塚が翌朝運営法人へ提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「運営法人への予算報告草案」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '運営法人への予算報告草案'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:40', victim_found_in = '解析室', victim_investigable = 1 WHERE victim_name = '瀬尾俊'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'launch-loss', 'at', '19:42', 'place', '甲板', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["launch-lost-1942","kariya-saw-launch-loss"]'), 'kind', 'solid'), json_object('id', 'launch-note', 'at', '19:48', 'place', '甲板', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["launch-note-signed"]'), 'kind', 'solid'), json_object('id', 'seo-call', 'at', '19:56', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '野々村岳' LIMIT 1)), 'facts', json('["seo-alive-1956","launch-gone-before-death"]'), 'kind', 'solid'), json_object('id', 'kariya-analysis', 'at', '20:14', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1)), 'facts', json('["kariya-entered-analysis-2014"]'), 'kind', 'claim'), json_object('id', 'seo-death', 'at', '20:18', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1)), 'facts', json('["seo-death-2018","kariya-killed-seo"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '20:22', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["mizuki-saw-kariya-2022"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:40', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '野々村岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["body-found-2040"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"瀬尾俊は解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「共同研究データの提出履歴」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '狩谷か野々村に瀬尾が直前まで確認していた研究データの提出経緯を尋ねたら開示する。または遺体・現場を調べ、「共同研究データの提出履歴」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '共同研究データの提出履歴'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:45', victim_found_in = '西側事務室', victim_investigable = 1 WHERE victim_name = '鷺沢修'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'submission-confrontation', 'at', '20:58', 'place', '会館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1)), 'facts', json('["kuga-stole-results","sagisawa-found-submission","sagisawa-confronted-kuga"]'), 'kind', 'claim'), json_object('id', 'sagisawa-death', 'at', '21:10', 'place', '西側事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1)), 'facts', json('["kuga-killed-sagisawa"]'), 'kind', 'claim'), json_object('id', 'west-corridor-sighting', 'at', '21:16', 'place', '西側事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["shiraishi-saw-kuga-west"]'), 'kind', 'solid'), json_object('id', 'rumor-starts', 'at', '21:18', 'place', '東資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1)), 'facts', json('["kuga-claimed-saw-east","kuga-started-rumor","no-independent-east-sighting"]'), 'kind', 'claim'), json_object('id', 'rumor-repeated', 'at', '21:22', 'place', '会館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["naruse-learned-from-kuga","naruse-repeated-rumor","shiraishi-learned-from-naruse"]'), 'kind', 'solid'), json_object('id', 'east-room-still-unused', 'at', '21:40', 'place', '東資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["east-room-unused"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:45', 'place', '西側事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["body-found-2145"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"鷺沢修は西側事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「単独名義の投稿原稿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '久我か成瀬に事件直前の投稿原稿と鷺沢との対立について尋ねたら開示する。または遺体・現場を調べ、「単独名義の投稿原稿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '単独名義の投稿原稿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:38', victim_found_in = '運行事務室', victim_investigable = 1 WHERE victim_name = '高瀬修司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'tool-case-placed', 'at', '21:09', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["cabin-display-weight-based","tool-case-heavy-enough","sudo-left-tool-case"]'), 'kind', 'solid'), json_object('id', 'occupancy-start', 'at', '21:10', 'place', '山頂駅', 'participants', json_array(), 'facts', json('["occupancy-display-on"]'), 'kind', 'solid'), json_object('id', 'sudo-leaves-cabin', 'at', '21:12', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-left-cabin-area"]'), 'kind', 'claim'), json_object('id', 'enomoto-sighting', 'at', '21:18', 'place', '職員通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '榎本澄' LIMIT 1)), 'facts', json('["enomoto-saw-sudo-2118"]'), 'kind', 'solid'), json_object('id', 'takase-death', 'at', '21:22', 'place', '運行事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-killed-takase"]'), 'kind', 'claim'), json_object('id', 'sudo-return', 'at', '21:28', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-returned-cabin"]'), 'kind', 'claim'), json_object('id', 'occupancy-end', 'at', '21:30', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["occupancy-display-on"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:38', 'place', '運行事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '長峰礼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '榎本澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '折原壮' LIMIT 1)), 'facts', json('["body-found-2138"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"高瀬修司は運行事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「制動点検記録と作業履歴の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '須藤か長峰に高瀬が事件直前に確認していた制動点検について尋ね、記録と実作業の不一致を追及したら開示する。または遺体・現場を調べ、「制動点検記録と作業履歴の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '制動点検記録と作業履歴の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:30', victim_found_in = '書斎', victim_investigable = 1 WHERE victim_name = '高瀬涼子'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'dinner-start', 'at', '19:00', 'place', 'dining', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["dinner-started-1900"]'), 'kind', 'solid'), json_object('id', 'fukagawa-leaves', 'at', '19:15', 'place', 'corridor', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["fukagawa-left-1915","fukagawa-at-phone-booth"]'), 'kind', 'solid'), json_object('id', 'ryoko-to-study', 'at', '19:20', 'place', 'study', 'participants', json_array(), 'facts', json('["ryoko-moved-to-study-1920"]'), 'kind', 'solid'), json_object('id', 'kiryu-argument', 'at', '19:35', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["kiryu-argued-with-ryoko-1935"]'), 'kind', 'solid'), json_object('id', 'fukagawa-returns', 'at', '19:45', 'place', 'dining', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["fukagawa-returned-1945"]'), 'kind', 'solid'), json_object('id', 'mizuki-poisons', 'at', '19:50', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["mizuki-poisoned-brandy-1950","kiryu-passed-mizuki-1950","mizuki-took-aconite"]'), 'kind', 'solid'), json_object('id', 'mizuki-serves', 'at', '20:00', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1)), 'facts', json('["mizuki-carried-brandy-2000","brandy-was-poisoned"]'), 'kind', 'solid'), json_object('id', 'ryoko-drinks', 'at', '20:15', 'place', 'study', 'participants', json_array(), 'facts', json('["ryoko-drank-at-2015"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:30', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1)), 'facts', json('["death-found-2030"]'), 'kind', 'solid')), victim_cause_of_death = '植物性の毒物による中毒死', victim_findings = json('[{"id":"no-struggle","statement":"争った跡が無い。着衣も髪も乱れておらず、文机の上も片付いたままになっている。","requires":{"revelations":[],"evidences":[]}},{"id":"numbness-signs","statement":"唇のまわりと指先に、しびれが出たときの跡が残っている。","requires":{"revelations":[],"evidences":[]}},{"id":"single-glass","statement":"文机に、飲みかけのグラスが一つだけ置かれている。誰かと酌み交わした跡は無い。","requires":{"revelations":[],"evidences":[]}},{"id":"heir-draft","statement":"硯箱の下に、書き直しかけの遺言書の草案が伏せてある。後継者の項に線が引かれ、余白に書き込みがある。","requires":{"revelations":[],"evidences":["will-record"]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'プレイヤーが遺体または書斎の文机まわりを調べ、探偵が硯箱の下の草案に触れたら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '書き直しかけの遺言書の草案'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:15', victim_found_in = '予備部品庫', victim_investigable = 1 WHERE victim_name = '真田啓介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'schedule-created', 'at', '21:52', 'place', '監視室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-created-scheduled-job"]'), 'kind', 'solid'), json_object('id', 'door-propped', 'at', '21:55', 'place', '予備部品庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '波多野結' LIMIT 1)), 'facts', json('["parts-door-propped"]'), 'kind', 'solid'), json_object('id', 'kuze-leaves', 'at', '21:58', 'place', '監視室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-left-noc"]'), 'kind', 'claim'), json_object('id', 'scheduled-execution', 'at', '22:00', 'place', '監視室', 'participants', json_array(), 'facts', json('["scheduled-jobs-ran"]'), 'kind', 'solid'), json_object('id', 'corridor-sighting', 'at', '22:01', 'place', '東側廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '真壁徹' LIMIT 1)), 'facts', json('["makabe-saw-kuze"]'), 'kind', 'solid'), json_object('id', 'sanada-death', 'at', '22:04', 'place', '予備部品庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-killed-sanada"]'), 'kind', 'claim'), json_object('id', 'kuze-return', 'at', '22:08', 'place', '監視室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-returned-noc"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '22:15', 'place', '予備部品庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '波多野結' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '甲田修' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '真壁徹' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"真田啓介は予備部品庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「交換部品の請求書と在庫表」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '久世か波多野に真田が事件直前に照合していた保守費と在庫について尋ねたら開示する。または遺体・現場を調べ、「交換部品の請求書と在庫表」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '交換部品の請求書と在庫表'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '01:29', victim_found_in = '会議室', victim_investigable = 1 WHERE victim_name = '芳賀俊介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'route-loaded', 'at', '00:57', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-repeat-route-capable","hiiragi-loaded-auto-route"]'), 'kind', 'solid'), json_object('id', 'drone-launch', 'at', '01:00', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-flew-auto","route-has-no-manual-input"]'), 'kind', 'solid'), json_object('id', 'hiiragi-leaves-roof', 'at', '01:02', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-left-roof"]'), 'kind', 'claim'), json_object('id', 'saegusa-sighting', 'at', '01:08', 'place', '連絡階段', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '三枝千尋' LIMIT 1)), 'facts', json('["saegusa-saw-hiiragi-0108"]'), 'kind', 'solid'), json_object('id', 'haga-death', 'at', '01:12', 'place', '会議室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-killed-haga"]'), 'kind', 'claim'), json_object('id', 'hiiragi-return', 'at', '01:17', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-returned-roof"]'), 'kind', 'claim'), json_object('id', 'drone-lands', 'at', '01:18', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-flew-auto"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '01:29', 'place', '会議室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '国分透' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '三枝千尋' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '波木恵' LIMIT 1)), 'facts', json('["body-found-0129"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"芳賀俊介は会議室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「過去点検写真との一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '柊木か三枝に芳賀が事件直前に確認していた点検写真について尋ね、過去画像との一致を追及したら開示する。または遺体・現場を調べ、「過去点検写真との一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '過去点検写真との一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '19:10', victim_found_in = '記録室', victim_investigable = 1 WHERE victim_name = 'ロレンツォ・ヴァーレ'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'lorenzo-audit', 'at', '18:35', 'place', '検疫島', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-diverted-medicine","lorenzo-found-shortage","lorenzo-confronted-marta"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '18:41', 'place', '記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ニコロ・フェッリ' LIMIT 1)), 'facts', json('["nicolo-saw-marta-before-bell"]'), 'kind', 'solid'), json_object('id', 'lorenzo-death', 'at', '18:44', 'place', '記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-killed-lorenzo"]'), 'kind', 'claim'), json_object('id', 'sunset-bell', 'at', '18:47', 'place', '鐘楼', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ピエトロ・サルヴィ' LIMIT 1)), 'facts', json('["bell-rang-1847","sunset-bell-not-fixed-hour"]'), 'kind', 'solid'), json_object('id', 'marta-returns', 'at', '18:49', 'place', '薬剤庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-returned-after-bell"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '19:10', 'place', '記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ニコロ・フェッリ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ピエトロ・サルヴィ' LIMIT 1)), 'facts', json('["body-found-1910"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"ロレンツォ・ヴァーレは記録室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「不足した薬剤の帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'マルタかニコロに薬剤の不足とロレンツォの直前の調査を尋ねたら開示する。または遺体・現場を調べ、「不足した薬剤の帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND label = '不足した薬剤の帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:30', victim_found_in = '測量室', victim_investigable = 1 WHERE victim_name = 'エドワード・ヘイル'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'hale-confronts-bell', 'at', '21:35', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-bid-fraud","helale-confronted-bell"]'), 'kind', 'claim'), json_object('id', 'bell-enters-tunnel', 'at', '21:52', 'place', '連絡坑道', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-left-shaft-one"]'), 'kind', 'solid'), json_object('id', 'thomas-sees-bell', 'at', '21:58', 'place', '第2立坑', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'トーマス・リード' LIMIT 1)), 'facts', json('["thomas-saw-bell-tunnel"]'), 'kind', 'solid'), json_object('id', 'hale-death', 'at', '22:03', 'place', '測量室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-killed-hale"]'), 'kind', 'claim'), json_object('id', 'false-telegram', 'at', '22:12', 'place', '第2立坑', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'クララ・ウェッブ' LIMIT 1)), 'facts', json('["bell-sent-telegram","telegram-received-2212"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:30', 'place', '測量室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'クララ・ウェッブ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'トーマス・リード' LIMIT 1)), 'facts', json('["body-found-2230"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"エドワード・ヘイルは測量室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「水増しされた資材帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'ベルにヘイルと直前に揉めた帳簿について追及するか、クララに二人の口論について尋ねたら開示する。または遺体・現場を調べ、「水増しされた資材帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND label = '水増しされた資材帳簿'; diff --git a/db/migrations/0012_evidence-detail-and-death-time.sql b/db/migrations/0012_evidence-detail-and-death-time.sql new file mode 100644 index 0000000..0738a75 --- /dev/null +++ b/db/migrations/0012_evidence-detail-and-death-time.sql @@ -0,0 +1,2 @@ +ALTER TABLE `evidences` ADD `description` text;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `victim_estimated_death_at` text; \ No newline at end of file diff --git a/db/migrations/0013_clash-materials.sql b/db/migrations/0013_clash-materials.sql new file mode 100644 index 0000000..26e2f6d --- /dev/null +++ b/db/migrations/0013_clash-materials.sql @@ -0,0 +1,2 @@ +ALTER TABLE `characters` ADD `lie_refs` text DEFAULT '[]' NOT NULL;--> statement-breakpoint +ALTER TABLE `evidences` ADD `contradicts` text DEFAULT '[]' NOT NULL; \ No newline at end of file diff --git a/db/migrations/0014_scenario-latest-authoring-upgrade.sql b/db/migrations/0014_scenario-latest-authoring-upgrade.sql new file mode 100644 index 0000000..b42d48a --- /dev/null +++ b/db/migrations/0014_scenario-latest-authoring-upgrade.sql @@ -0,0 +1,949 @@ +UPDATE scenarios SET title = '雪籠りの白樺峰', victim_found_in = '支配人室', victim_estimated_death_at = NULL WHERE victim_name = '早瀬隆司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'audit-warning', 'at', '21:05', 'place', '山荘内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["hayase-audit-next-morning","natsume-cash-shortage"]'), 'kind', 'claim'), json_object('id', 'fake-message', 'at', '21:16', 'place', 'フロント', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["fake-message-left","no-214-call","paper-from-night-pad"]'), 'kind', 'solid'), json_object('id', 'ask-oda', 'at', '21:24', 'place', '客室棟', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["natsume-asked-oda"]'), 'kind', 'solid'), json_object('id', 'management-corridor', 'at', '21:27', 'place', '管理廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["oda-saw-natsume-office-side"]'), 'kind', 'solid'), json_object('id', 'hayase-death', 'at', '21:30', 'place', '支配人室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["hayase-death-2130","natsume-killed-hayase"]'), 'kind', 'claim'), json_object('id', 'ask-fujino', 'at', '21:31', 'place', '厨房前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '藤野修平' LIMIT 1)), 'facts', json('["natsume-asked-fujino"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:45', 'place', '支配人室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '藤野修平' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["body-found-2145"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"natsume-believed-room-214","about":"natsume-knew-renumbering"},{"id":"natsume-no-office","about":"oda-saw-natsume-office-side"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"oda-linen-clean","about":"oda-hid-broken-linen"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"fujino-no-wine","about":"fujino-took-wine"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '藤野修平'; +--> statement-breakpoint +UPDATE evidences SET description = '三年前の改装で二一四号室は廃止され、その案内担当者欄には夏目の名前がある。', contradicts = json('["lie:natsume-believed-room-214"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '改装後の客室番号表'; +--> statement-breakpoint +UPDATE evidences SET description = '二一四号室を名乗る着信はなく、設備苦情は電話ではなく紙の伝言だけで現れている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '館内電話の着信一覧'; +--> statement-breakpoint +UPDATE evidences SET description = '二一四号室の伝言紙は、夏目が夜勤日誌で使うメモ束から切り離された紙と一致する。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '伝言紙の裁断痕'; +--> statement-breakpoint +UPDATE evidences SET description = '小田は、客室棟を探しているはずの夏目が支配人室へ続く管理廊下から出てくるのを見ている。', contradicts = json('["lie:natsume-no-office"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '二十一時二十七分の管理廊下目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '早瀬の予定表には、翌朝最初の業務として夏目の夜勤売上不足を確認する予定が記されている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '翌朝の会計監査メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '小田が破損品の処理を隠すため、一部の交換記録を書き換えていたことが分かる。事件とは独立した隠し事である。', contradicts = json('["lie:oda-linen-clean"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '書き換えられたリネン交換記録'; +--> statement-breakpoint +UPDATE evidences SET description = '藤野が料理用ワインを入れた袋が見つかるが、事件時刻の支配人室とは関係しない。', contradicts = json('["lie:fujino-no-wine"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '厨房裏の持ち帰り袋'; +--> statement-breakpoint +UPDATE scenarios SET title = '雪嶺修道院', victim_found_in = '資料整理室', victim_estimated_death_at = NULL WHERE victim_name = '高瀬静一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'billing-confrontation', 'at', '20:48', 'place', '修道院内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1)), 'facts', json('["repair-overbilling","mizuki-overbilled","takase-confronted-mizuki"]'), 'kind', 'claim'), json_object('id', 'takase-death', 'at', '21:05', 'place', '資料整理室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1)), 'facts', json('["mizuki-killed-takase"]'), 'kind', 'claim'), json_object('id', 'bell-rung', 'at', '21:20', 'place', '修道院内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '黒川玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["mizuki-rang-bell-remotely","all-heard-bell","no-one-saw-takase-bell"]'), 'kind', 'solid'), json_object('id', 'test-line-clue', 'at', '21:27', 'place', '聖歌席裏', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["temporary-test-line-exists","genda-thought-line-removed","test-line-tag-remained"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '資料整理室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '黒川玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mizuki-bell-alibi","about":"mizuki-rang-bell-remotely"},{"id":"mizuki-no-overbilling","about":"mizuki-overbilled"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kurokawa-no-photo","about":"kurokawa-secret-photo"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '黒川玲'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"genda-no-copy-key","about":"genda-secret-key-copy"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修'; +--> statement-breakpoint +UPDATE evidences SET description = '修復中は聖歌席裏の仮設試験線から鐘の作動確認ができることが図面に記されている。', contradicts = json('["lie:mizuki-bell-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '鐘塔修復の試験系統図'; +--> statement-breakpoint +UPDATE evidences SET description = '点検口には仮設試験線がまだ接続中であることを示す工事タグが残っている。', contradicts = json('["lie:mizuki-bell-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '聖歌席裏の工事タグ'; +--> statement-breakpoint +UPDATE evidences SET description = '三人とも鐘を聞いているが、その時刻に高瀬本人が鐘塔へ向かう姿を見た者はいない。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '九時二十分の目撃不在'; +--> statement-breakpoint +UPDATE evidences SET description = '実施記録のない追加工事が水城の承認で請求され、高瀬がその項目へ印を付けている。', contradicts = json('["lie:mizuki-no-overbilling"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '修復費の追加請求一覧'; +--> statement-breakpoint +UPDATE evidences SET description = '黒川の端末には撮影禁止の古文書画像が残るが、鐘の時刻とは関係しない。', contradicts = json('["lie:kurokawa-no-photo"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '撮影禁止史料の画像'; +--> statement-breakpoint +UPDATE evidences SET description = '玄田が作った予備鍵が見つかるが、九時二十分の鐘は鍵を使わず鳴らせたため主経路ではない。', contradicts = json('["lie:genda-no-copy-key"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '無許可の予備鍵'; +--> statement-breakpoint +UPDATE scenarios SET title = '白雪研修館', victim_found_in = '書斎', victim_estimated_death_at = NULL WHERE victim_name = '塚本誠'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'fraud-discovered', 'at', '21:05', 'place', 'ロッジ内', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["payment-fraud","tsukamoto-found-fraud"]'), 'kind', 'claim'), json_object('id', 'summon-note', 'at', '21:10', 'place', '書斎', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1)), 'facts', json('["tsukamoto-summoned-katase"]'), 'kind', 'claim'), json_object('id', 'katase-leaves-game', 'at', '21:12', 'place', '談話室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["katase-left-2112","mamiya-substituted-blue","game-tracks-seats"]'), 'kind', 'solid'), json_object('id', 'tsukamoto-death', 'at', '21:18', 'place', '書斎', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1)), 'facts', json('["katase-killed-tsukamoto"]'), 'kind', 'claim'), json_object('id', 'score-continues', 'at', '21:21', 'place', 'ロッジ内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["blue-score-continued","yoshioka-copied-score-later"]'), 'kind', 'solid'), json_object('id', 'katase-returns', 'at', '21:25', 'place', '談話室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["katase-returned-2125","todo-saw-katase-return"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '書斎', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"katase-never-left-game","about":"katase-left-2112"},{"id":"katase-no-fake-fee","about":"payment-fraud"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"todo-no-plagiarism","about":"todo-secret-plagiarism"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"yoshioka-no-private-expense","about":"yoshioka-secret-expense"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mamiya-no-storage-room","about":"mamiya-secret-room"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵'; +--> statement-breakpoint +UPDATE evidences SET description = '表は青・赤・白・黄の席ごとに得点を記録し、途中で座った人物名は残さない形式である。', contradicts = json('["lie:katase-never-left-game"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '四色の席別得点表'; +--> statement-breakpoint +UPDATE evidences SET description = '間宮は21時13分から21時24分ごろまで片瀬の代わりに青席へ座っていた。', contradicts = json('["lie:katase-never-left-game"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '青席の代打証言'; +--> statement-breakpoint +UPDATE evidences SET description = '藤堂は片瀬が廊下側から談話室へ戻って青席へ座り直すところを見ている。', contradicts = json('["lie:katase-never-left-game"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '二十一時二十五分の帰席目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '実在しない追加講師枠が片瀬の処理で計上され、塚本が該当欄に確認印を付けている。', contradicts = json('["lie:katase-no-fake-fee"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '外部講師費の精算書'; +--> statement-breakpoint +UPDATE evidences SET description = '藤堂の資料には他者の文章を出典表示なしで転用した部分があるが、事件のアリバイとは関係しない。', contradicts = json('["lie:todo-no-plagiarism"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '出典のない研修資料'; +--> statement-breakpoint +UPDATE evidences SET description = '吉岡が私用の交通費を研修経費へ混ぜていたことが分かるが、主事件とは独立している。', contradicts = json('["lie:yoshioka-no-private-expense"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '私用交通費の精算'; +--> statement-breakpoint +UPDATE evidences SET description = '間宮が空き客室を私物置き場として使っていたことが分かるが、片瀬の離席とは無関係である。', contradicts = json('["lie:mamiya-no-storage-room"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '空き客室の私物'; +--> statement-breakpoint +UPDATE scenarios SET title = '地底研究所', victim_found_in = '地図解析室', victim_estimated_death_at = NULL WHERE victim_name = '岩代圭吾'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'tag-attached-cart', 'at', '20:58', 'place', '研究所内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["locator-tag-removable","kagawa-tag-on-cart"]'), 'kind', 'solid'), json_object('id', 'cart-loop-start', 'at', '21:00', 'place', '測量区画', 'room', '', 'record', '位置履歴', 'participants', json_array(), 'facts', json('["mapping-cart-auto-loop","tag-track-matches-cart"]'), 'kind', 'solid'), json_object('id', 'kagawa-leaves', 'at', '21:01', 'place', '測量区画', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-left-survey-zone"]'), 'kind', 'claim'), json_object('id', 'tono-sighting', 'at', '21:07', 'place', '連絡通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '遠野澪' LIMIT 1)), 'facts', json('["tono-saw-kagawa-2107"]'), 'kind', 'solid'), json_object('id', 'iwashiro-death', 'at', '21:11', 'place', '地図解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-killed-iwashiro"]'), 'kind', 'claim'), json_object('id', 'cart-loop-end', 'at', '21:15', 'place', '測量区画', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["mapping-cart-auto-loop","tag-track-matches-cart"]'), 'kind', 'solid'), json_object('id', 'tag-recovered', 'at', '21:17', 'place', '測量区画', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-recovered-tag"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '21:27', 'place', '地図解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '新堂匠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '結城真' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '遠野澪' LIMIT 1)), 'facts', json('["body-found-2127"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kagawa-location-alibi","about":"kagawa-left-survey-zone"},{"id":"kagawa-wore-tag","about":"kagawa-tag-on-cart"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"shindo-no-private-sample","about":"shindo-hid-sample"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '新堂匠'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"yuki-no-bypass","about":"yuki-bypassed-sensor"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '結城真'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"tono-no-edit","about":"tono-edited-time-note"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '遠野澪'; +--> statement-breakpoint +UPDATE evidences SET description = '香川の位置タグと自動測量カートが、21時00分から15分まで同じ地点を同じ時刻に通過している。二つの軌跡は実質的に重なる。', contradicts = json('["lie:kagawa-location-alibi","lie:kagawa-wore-tag"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '位置タグと自動測量カートの軌跡比較'; +--> statement-breakpoint +UPDATE evidences SET description = '遠野は21時07分ごろ、位置タグを胸元に付けていない香川を解析室側の連絡通路で見ている。', contradicts = json('["lie:kagawa-location-alibi","lie:kagawa-wore-tag"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '二十一時七分のタグなし目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '香川が深部採取として登録した試料のラベルが、実際の測量データでは浅い区画の座標と一致する。岩代の訂正予定も残る。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '試料ラベルと測量座標の不一致'; +--> statement-breakpoint +UPDATE evidences SET description = '共同管理の試料が新堂の個人ケースから見つかるが、解析室の事件とは独立した規約違反である。', contradicts = json('["lie:shindo-no-private-sample"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '新堂の個人ケースにある共同試料'; +--> statement-breakpoint +UPDATE evidences SET description = '結城が換気設備の一つのセンサーを手順外で無効化していたことが分かるが、事件とは別件である。', contradicts = json('["lie:yuki-no-bypass"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '結城の換気センサー無効化記録'; +--> statement-breakpoint +UPDATE evidences SET description = '夕方の巡回時刻が遠野によって後から書き直されているが、21時07分の目撃とは無関係の記録漏れだった。', contradicts = json('["lie:tono-no-edit"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '遠野の安全記録修正履歴'; +--> statement-breakpoint +UPDATE scenarios SET title = '白樺館、四十七年', victim_found_in = '書斎', victim_estimated_death_at = '22:05' WHERE victim_name = '野上修一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'fraud-confrontation', 'at', '21:50', 'place', '山荘内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-forged-expenses","nogami-found-megumi-fraud"]'), 'kind', 'claim'), json_object('id', 'nogami-death', 'at', '22:05', 'place', '書斎', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-killed-nogami"]'), 'kind', 'claim'), json_object('id', 'maki-sees-megumi', 'at', '22:08', 'place', '廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["maki-saw-megumi-study"]'), 'kind', 'solid'), json_object('id', 'megumi-goes-room', 'at', '22:15', 'place', '自室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-originally-asleep-2215"]'), 'kind', 'claim'), json_object('id', 'false-fireplace-sighting', 'at', '22:30', 'place', '暖炉前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1)), 'facts', json('["maki-lied-fireplace","original-only-maki-claimed-sighting"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:10', 'place', '書斎', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '藤村達也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["body-found-2310"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"maki-real-sighting","about":"maki-lied-fireplace"},{"id":"maki-no-theft","about":"maki-stole-cash"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"tatsuya-no-affair","about":"tatsuya-secret-affair"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '藤村達也'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"megumi-no-fraud","about":"megumi-forged-expenses"},{"id":"megumi-never-study","about":"maki-saw-megumi-study"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵'; +--> statement-breakpoint +UPDATE evidences SET description = '真紀だけが22時30分の直接目撃を主張し、藤村は真紀から聞いたと述べ、倉田は22時15分から眠っていたとしている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND label = '1979年2月18日の三人の供述調書'; +--> statement-breakpoint +UPDATE evidences SET description = '「三人が暖炉前の野上を見た」と誤って要約した記事が、その後何度も事件紹介で引用されている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND label = '一週間後の新聞記事'; +--> statement-breakpoint +UPDATE evidences SET description = '事件翌日の帳簿には少額の現金不足があり、真紀の最初の供述では22時台の行動が不自然に曖昧になっている。', contradicts = json('["lie:maki-real-sighting","lie:maki-no-theft"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND label = '現金箱の不足メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '事件翌朝に真紀が描いた簡単な館内図には、22時08分ごろ書斎側で倉田とすれ違った印が残っている。', contradicts = json('["lie:megumi-never-study"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND label = '真紀の最初の館内スケッチ'; +--> statement-breakpoint +UPDATE evidences SET description = '倉田の担当欄で仕入れ額が実際より増やされ、野上が事件当日に再確認の印を付けている。', contradicts = json('["lie:megumi-no-fraud"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND label = '仕入れ帳の水増し'; +--> statement-breakpoint +UPDATE scenarios SET title = '深海区画アビス3', victim_found_in = '資料室', victim_estimated_death_at = NULL WHERE victim_name = '篠宮亮'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'readings-start', 'at', '00:00', 'place', '制御室', 'room', '', 'record', '点検記録', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["readings-automatic","signatures-batchable"]'), 'kind', 'solid'), json_object('id', 'sagisawa-leaves', 'at', '00:01', 'place', '制御室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-left-control"]'), 'kind', 'claim'), json_object('id', 'passage-sighting', 'at', '00:05', 'place', '中央通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '小日向茜' LIMIT 1)), 'facts', json('["kohinata-saw-sagisawa"]'), 'kind', 'solid'), json_object('id', 'shinomiya-death', 'at', '00:08', 'place', '資料室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-killed-shinomiya"]'), 'kind', 'claim'), json_object('id', 'sagisawa-return', 'at', '00:14', 'place', '制御室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-returned-control"]'), 'kind', 'claim'), json_object('id', 'batch-sign', 'at', '00:16', 'place', '居住区内', 'room', '', 'record', '署名記録', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-batch-signed","one-signature-transaction"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '00:22', 'place', '資料室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鳴海俊' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '小日向茜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '土岐誠' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sagisawa-control-alibi","about":"sagisawa-left-control"},{"id":"sagisawa-each-signature","about":"sagisawa-batch-signed"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"narumi-no-private-sample","about":"narumi-hid-sample"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鳴海俊'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kohinata-no-deletion","about":"kohinata-deleted-message"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '小日向茜'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"toki-maintenance-current","about":"toki-bypassed-maintenance"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '土岐誠'; +--> statement-breakpoint +UPDATE evidences SET description = '00時00分から00時12分までの七つの署名は同じ処理番号を持ち、00時16分に一度の操作で登録されている。', contradicts = json('["lie:sagisawa-control-alibi","lie:sagisawa-each-signature"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '点検画面の確認処理番号'; +--> statement-breakpoint +UPDATE evidences SET description = '小日向は00時05分ごろ、中央通路で鷺沢とすれ違っている。', contradicts = json('["lie:sagisawa-control-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '零時五分の中央通路目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '鷺沢の点検表では交換済みの部品が、実際には古い個体番号のまま残っている。篠宮の調査メモには帰還後の報告予定がある。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '部品交換記録と実機番号の不一致'; +--> statement-breakpoint +UPDATE evidences SET description = '共同保管対象の試料が鳴海の個人ケースから見つかるが、資料室の事件とは独立した規約違反である。', contradicts = json('["lie:narumi-no-private-sample"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '鳴海が隠した地質試料'; +--> statement-breakpoint +UPDATE evidences SET description = '小日向の端末から一件の通信記録が削除されているが、事件とは無関係の手順ミスに関するものだった。', contradicts = json('["lie:kohinata-no-deletion"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '小日向の通信記録削除履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '土岐が期限切れの昇降機構を応急処置で使っていたことが分かるが、支援船との接続は事件前に解除されている。', contradicts = json('["lie:toki-maintenance-current"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '土岐の期限切れ整備票'; +--> statement-breakpoint +UPDATE scenarios SET title = '霧航船しおかぜ', victim_found_in = '上部ラウンジ', victim_estimated_death_at = NULL WHERE victim_name = '柴田功'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'passenger-leaves', 'at', '16:35', 'place', '船内', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["passenger-left-before-departure","booked-passengers-42"]'), 'kind', 'solid'), json_object('id', 'shibata-warning', 'at', '16:48', 'place', '船内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-skimmed-sales","shibata-found-shortage","shibata-warned-kanda"]'), 'kind', 'claim'), json_object('id', 'secret-interview', 'at', '16:50', 'place', '上部ラウンジ', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '藤原奈緒' LIMIT 1)), 'facts', json('["fujiwara-secret-interview"]'), 'kind', 'solid'), json_object('id', 'count-sheet-made', 'at', '16:58', 'place', '船内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-wrote-count-sheet"]'), 'kind', 'claim'), json_object('id', 'stair-sighting', 'at', '17:03', 'place', '階段', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '荻原陸' LIMIT 1)), 'facts', json('["ogiwara-saw-kanda-1703"]'), 'kind', 'solid'), json_object('id', 'shibata-death', 'at', '17:06', 'place', '上部ラウンジ', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-killed-shibata-1706"]'), 'kind', 'claim'), json_object('id', 'kanda-returns', 'at', '17:11', 'place', '下部客室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-returned-lower-1711","count-sheet-says-42"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '17:20', 'place', '上部ラウンジ', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '荻原陸' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '藤原奈緒' LIMIT 1)), 'facts', json('["body-found-1720"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kanda-passenger-count-alibi","about":"kanda-wrote-count-sheet"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"ogiwara-no-guest-access","about":"ogiwara-let-friend-bridge"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '荻原陸'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"fujiwara-no-meeting","about":"fujiwara-secret-interview"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '藤原奈緒'; +--> statement-breakpoint +UPDATE evidences SET description = '予約名簿は42人だが、16時35分に一人が下船しており、実際の乗客は41人だった。', contradicts = json('["lie:kanda-passenger-count-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND label = '出航前の乗降記録'; +--> statement-breakpoint +UPDATE evidences SET description = '荻原は17時03分ごろ、下部客室ではなく上部ラウンジへ向かう神田とすれ違っている。', contradicts = json('["lie:kanda-passenger-count-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND label = '十七時三分の階段の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '現金で販売された追加券の枚数に対し、帳簿へ記録された売上が継続的に少ない。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND label = '追加券の控えと売上帳簿'; +--> statement-breakpoint +UPDATE evidences SET description = '16時50分から柴田と会い、安全管理の内部資料を受け取った記録があるが、16時56分には面会を終えている。', contradicts = json('["lie:fujiwara-no-meeting"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND label = '藤原の取材メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '荻原の知人が乗員専用区域から撮った写真があり、荻原の規則違反は分かるが事件時刻とは無関係である。', contradicts = json('["lie:ogiwara-no-guest-access"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND label = '乗員通路から撮られた出航前の写真'; +--> statement-breakpoint +UPDATE scenarios SET title = '宵祭り', victim_found_in = '祭具倉庫', victim_estimated_death_at = NULL WHERE victim_name = '神谷宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kamiya-warning', 'at', '19:50', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["kamiya-warned-tozuka","kamiya-found-kickbacks"]'), 'kind', 'claim'), json_object('id', 'blackout-start', 'at', '20:18', 'place', '会場', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["blackout-started-2018","lighting-preset-ran"]'), 'kind', 'solid'), json_object('id', 'tozuka-leaves', 'at', '20:20', 'place', '祭具倉庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-left-console-2020"]'), 'kind', 'claim'), json_object('id', 'aihara-sighting', 'at', '20:22', 'place', '倉庫前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["aihara-saw-tozuka-2022"]'), 'kind', 'solid'), json_object('id', 'kamiya-death', 'at', '20:23', 'place', '祭具倉庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-killed-kamiya-2023","spare-key-borrowed"]'), 'kind', 'claim'), json_object('id', 'tozuka-returns', 'at', '20:25', 'place', '操作卓', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-returned-2025"]'), 'kind', 'claim'), json_object('id', 'lights-return', 'at', '20:26', 'place', '会場', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["lights-restored-2026"]'), 'kind', 'solid'), json_object('id', 'key-check', 'at', '20:28', 'place', '境内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1)), 'facts', json('["makabe-checked-key-2028"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:35', 'place', '祭具倉庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["body-found-2035"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"tozuka-console-alibi","about":"tozuka-left-console-2020"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"makabe-perfect-key-control","about":"spare-key-borrowed"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"aihara-saw-nothing","about":"aihara-saw-tozuka-2022"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆'; +--> statement-breakpoint +UPDATE evidences SET description = '20時18分から20時26分まで、制御盤が登録済みの復旧シーケンスを自動実行していた記録が残る。', contradicts = json('["lie:tozuka-console-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND label = '照明制御盤の自動復旧ログ'; +--> statement-breakpoint +UPDATE evidences SET description = '20時22分ごろ、相原は祭具倉庫方向へ急ぐ戸塚を非常灯の下で見ている。', contradicts = json('["lie:tozuka-console-alibi","lie:aihara-saw-nothing"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND label = '非常灯の下の反射ベスト'; +--> statement-breakpoint +UPDATE evidences SET description = '戸塚は準備期間中に予備鍵を借りており、その際に鍵箱の暗証番号を知る機会があった。', contradicts = json('["lie:makabe-perfect-key-control"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND label = '祭具倉庫の予備鍵の貸出記録'; +--> statement-breakpoint +UPDATE evidences SET description = '同規模の設備と比べて発注額が不自然に高く、戸塚へ還流した謝礼を神谷が照合したメモが挟まれている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND label = '照明設備の発注帳簿'; +--> statement-breakpoint +UPDATE evidences SET description = '真壁が修繕費へ一時流用した金額が確認できるが、神谷の死亡時刻や停電とは関係がない。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND label = '神社の寄付金収支の不足'; +--> statement-breakpoint +UPDATE scenarios SET title = '高潮の文書館', victim_found_in = '資料庫', victim_estimated_death_at = NULL WHERE victim_name = '今泉孝臣'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'vault-opened', 'at', '21:09', 'place', '希少資料庫', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["vault-key-only-opens","vault-self-locks","vault-open-2109","vault-stayed-open"]'), 'kind', 'solid'), json_object('id', 'yagami-enters', 'at', '21:14', 'place', '資料庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1)), 'facts', json('["yagami-entered-vault-2114","kuga-saw-yagami-2114"]'), 'kind', 'solid'), json_object('id', 'kuga-vault-front', 'at', '21:14', 'place', '資料庫前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '久我遼' LIMIT 1)), 'facts', json('["kuga-saw-yagami-2114"]'), 'kind', 'solid'), json_object('id', 'imaizumi-enters', 'at', '21:17', 'place', '資料庫', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["imaizumi-entered-vault-2117"]'), 'kind', 'solid'), json_object('id', 'imaizumi-death', 'at', '21:20', 'place', '希少資料庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1)), 'facts', json('["yagami-killed-imaizumi"]'), 'kind', 'claim'), json_object('id', 'yagami-exits', 'at', '21:22', 'place', '資料庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1)), 'facts', json('["yagami-left-vault-2122","vault-closed-2122"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '資料庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '戸塚誠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '久我遼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '真島香苗' LIMIT 1)), 'facts', json('["key-found-on-imaizumi","body-found-2140"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"yagami-restoration-room-alibi","about":"yagami-entered-vault-2114"},{"id":"yagami-key-needed-to-lock","about":"vault-key-only-opens"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kuga-no-damage","about":"kuga-tore-document"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '久我遼'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"tozuka-no-camera-stop","about":"tozuka-disabled-camera"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '戸塚誠'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"majima-no-photo-swap","about":"majima-hid-photo-swap"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '真島香苗'; +--> statement-breakpoint +UPDATE evidences SET description = '廊下側から開ける時だけ館長鍵が必要で、外へ出て扉を閉めるとラッチが自動で掛かる。施錠操作に鍵は不要である。', contradicts = json('["lie:yagami-key-needed-to-lock"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '希少資料庫の自動施錠仕様'; +--> statement-breakpoint +UPDATE evidences SET description = '資料庫の扉は21時09分に開き、21時22分に閉じるまで十三分間連続して開いていた。途中の再開錠は必要なかった。', contradicts = json('["lie:yagami-restoration-room-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '二十一時九分から二十二分の扉記録'; +--> statement-breakpoint +UPDATE evidences SET description = '久我は21時14分ごろ、搬入のため開いたままの資料庫へ八神が鍵を使わず入るのを見ている。', contradicts = json('["lie:yagami-restoration-room-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '二十一時十四分の八神入室目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '八神の処置記録では完了したはずの工程が資料の実状態と合わず、今泉が翌朝の担当変更と外部委員提出を記している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '修復処置記録と資料状態の不一致'; +--> statement-breakpoint +UPDATE evidences SET description = '傷めた資料が久我の整理箱から見つかるが、希少資料庫の事件とは独立した不始末である。', contradicts = json('["lie:kuga-no-damage"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '久我が隠した損傷資料'; +--> statement-breakpoint +UPDATE evidences SET description = '戸塚が私用の読書を隠すため閲覧室カメラを一時停止していたことが分かるが、希少資料庫とは別区画である。', contradicts = json('["lie:tozuka-no-camera-stop"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '戸塚の閲覧室カメラ停止記録'; +--> statement-breakpoint +UPDATE evidences SET description = '真島が鑑定の誤りを隠すため提出写真を差し替えていたことが分かるが、今泉の死亡とは別件である。', contradicts = json('["lie:majima-no-photo-swap"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '真島の来歴写真差し替え履歴'; +--> statement-breakpoint +UPDATE scenarios SET title = '水際の旧南央裁判所', victim_found_in = '保存記録室', victim_estimated_death_at = NULL WHERE victim_name = '磯崎章'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'door-opened-for-maintenance', 'at', '19:50', 'place', '保存記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1)), 'facts', json('["door-held-open-1950","door-remained-open-2022","no-new-unlock"]'), 'kind', 'solid'), json_object('id', 'mitamura-warning', 'at', '20:00', 'place', '保存記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["mitamura-told-yabuki-open"]'), 'kind', 'solid'), json_object('id', 'yabuki-enters', 'at', '20:09', 'place', '保存記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["yabuki-entered-unlogged"]'), 'kind', 'claim'), json_object('id', 'isozaki-death', 'at', '20:12', 'place', '保存記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["isozaki-death-2012","yabuki-killed-isozaki"]'), 'kind', 'claim'), json_object('id', 'corridor-return', 'at', '20:16', 'place', '廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '木瀬亮' LIMIT 1)), 'facts', json('["kise-saw-yabuki-return-2016"]'), 'kind', 'solid'), json_object('id', 'door-closes', 'at', '20:22', 'place', '保存記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1)), 'facts', json('["door-remained-open-2022"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:35', 'place', '保存記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '木瀬亮' LIMIT 1)), 'facts', json('["body-found-2035"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"yabuki-log-alibi","about":"yabuki-entered-unlogged"},{"id":"yabuki-no-corridor","about":"kise-saw-yabuki-return-2016"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mitamura-full-check","about":"mitamura-skipped-check"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kise-no-private-reading","about":"kise-read-private-file"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '木瀬亮'; +--> statement-breakpoint +UPDATE evidences SET description = '19時50分から20時22分まで扉は完全には閉じておらず、職員証を使わず通過できる状態だった。', contradicts = json('["lie:yabuki-log-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '保存記録室の扉状態記録'; +--> statement-breakpoint +UPDATE evidences SET description = '三田村は20時ごろ、矢吹本人に保存記録室の扉が開いていると伝えていた。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '点検中の開放を伝えた記録'; +--> statement-breakpoint +UPDATE evidences SET description = '木瀬は保存記録室側の廊下から戻る矢吹を20時16分ごろに目撃している。', contradicts = json('["lie:yabuki-no-corridor","lie:yabuki-log-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '二十時十六分の廊下目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '磯崎の端末に、矢吹による不適切な記録修正を翌朝報告するための草案が残っている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '監督部署への報告草案'; +--> statement-breakpoint +UPDATE evidences SET description = '三田村が点検の一項目を実施せず済ませたことが分かるが、保存記録室への矢吹の出入りとは独立している。', contradicts = json('["lie:mitamura-full-check"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '未実施の点検項目'; +--> statement-breakpoint +UPDATE evidences SET description = '木瀬が私的興味で古い事件資料を読んでいたことが分かるが、磯崎の死とは結びつかない。', contradicts = json('["lie:kise-no-private-reading"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '警備員の閲覧履歴'; +--> statement-breakpoint +UPDATE scenarios SET title = '青環美術館夜想', victim_found_in = '収蔵庫前', victim_estimated_death_at = NULL WHERE victim_name = '鳥羽薫'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'forgery-discovered', 'at', '18:25', 'place', '美術館内', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["toba-found-forgery"]'), 'kind', 'claim'), json_object('id', 'sakaki-confronted', 'at', '18:29', 'place', '美術館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["toba-confronted-sakaki"]'), 'kind', 'claim'), json_object('id', 'mido-bribe', 'at', '18:32', 'place', '美術館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '御堂和也' LIMIT 1)), 'facts', json('["mido-offered-bribe"]'), 'kind', 'claim'), json_object('id', 'uv-test-start', 'at', '18:35', 'place', '修復室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["uv-test-started"]'), 'kind', 'solid'), json_object('id', 'sakaki-leaves', 'at', '18:38', 'place', '修復室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["uv-lamp-off-1837","sakaki-left-restoration"]'), 'kind', 'solid'), json_object('id', 'enomoto-sighting', 'at', '18:44', 'place', '収蔵庫前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榎本駿' LIMIT 1)), 'facts', json('["enomoto-saw-sakaki-1844"]'), 'kind', 'solid'), json_object('id', 'toba-death', 'at', '18:46', 'place', '収蔵庫前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["sakaki-killed-toba-1846"]'), 'kind', 'claim'), json_object('id', 'sakaki-returns', 'at', '18:50', 'place', '修復室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["sakaki-returned-1850","uv-test-resumed"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '18:55', 'place', '収蔵庫前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '御堂和也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榎本駿' LIMIT 1)), 'facts', json('["body-found-1855"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sakaki-uv-alibi","about":"sakaki-left-restoration"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mido-only-greeting","about":"mido-offered-bribe"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '御堂和也'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"enomoto-no-doze","about":"enomoto-dozed"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榎本駿'; +--> statement-breakpoint +UPDATE evidences SET description = '装置は18時35分に起動したが、18時37分から18時51分まで停止している。', contradicts = json('["lie:sakaki-uv-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND label = '紫外線検査装置の電源履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '榎本は18時44分ごろ、収蔵庫前の通路で榊を見ている。', contradicts = json('["lie:sakaki-uv-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND label = '十八時四十四分の収蔵庫前の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '展示予定作品の額装と絵具層が過去の記録と一致せず、鳥羽が「原画ではない可能性」と書き残している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND label = '鳥羽の額装検査メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '御堂が鳥羽へ提示した謝礼額のメモ。拒否されたことは分かるが、事件時刻の行動とは結びつかない。', contradicts = json('["lie:mido-only-greeting"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND label = '御堂の謝礼額を書いたメモ'; +--> statement-breakpoint +UPDATE evidences SET description = '18時35分から18時42分ごろまで榎本の端末操作がなく、本人も短時間の居眠りを認めるが、18時44分の目撃とは両立する。', contradicts = json('["lie:enomoto-no-doze"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND label = '警備控室端末の無操作記録'; +--> statement-breakpoint +UPDATE scenarios SET title = '世代船アステリア', victim_found_in = '種子保管区', victim_estimated_death_at = '05:57' WHERE victim_name = 'ミラ・ヴォス'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'mira-confronts-sera', 'at', '05:45', 'place', '船内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["sera-seed-diversion","mira-found-diversion","mira-would-audit"]'), 'kind', 'claim'), json_object('id', 'dario-sees-sera', 'at', '05:52', 'place', '連絡路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ダリオ・ケイン' LIMIT 1)), 'facts', json('["dario-saw-sera-0552"]'), 'kind', 'solid'), json_object('id', 'mira-death', 'at', '05:57', 'place', '種子保管区', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["sera-killed-mira"]'), 'kind', 'claim'), json_object('id', 'agriculture-six', 'at', '06:00', 'place', '農業区', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["agriculture-dawn-0600-local","sera-claimed-after-six"]'), 'kind', 'solid'), json_object('id', 'medical-six', 'at', '06:20', 'place', '医療区', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ユナ・パク' LIMIT 1)), 'facts', json('["medical-dawn-0600-local"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '06:28', 'place', '種子保管区', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ダリオ・ケイン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ユナ・パク' LIMIT 1)), 'facts', json('["body-found-0628"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sera-six-alibi","about":"sera-claimed-after-six"},{"id":"sera-no-diversion","about":"sera-seed-diversion"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"dario-no-overload","about":"dario-hidden-overload"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ダリオ・ケイン'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"yuna-no-private-supply","about":"yuna-private-medication"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ユナ・パク'; +--> statement-breakpoint +UPDATE evidences SET description = '農業区06時00分は標準時05時40分、中央区06時00分は06時00分、医療区06時00分は06時20分に対応する。', contradicts = json('["lie:sera-six-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND label = '区画別の人工昼夜スケジュール'; +--> statement-breakpoint +UPDATE evidences SET description = 'ダリオの巡回記録と本人の証言から、標準時05時52分に種子保管区へ向かうセラを見たことが確認できる。', contradicts = json('["lie:sera-six-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND label = '電力巡回の位置記録'; +--> statement-breakpoint +UPDATE evidences SET description = 'セラ管理の希少種子だけ在庫と割当先が合わず、ミラが中央評議会への報告を準備している。', contradicts = json('["lie:sera-no-diversion"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND label = '希少種子の監査記録'; +--> statement-breakpoint +UPDATE evidences SET description = 'ユナが友人へ融通した用品の記録が見つかるが、種子保管区の事件とは独立している。', contradicts = json('["lie:yuna-no-private-supply"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND label = '手続き外の医療用品'; +--> statement-breakpoint +UPDATE scenarios SET title = '緑苑植物園', victim_found_in = '標本庫', victim_estimated_death_at = NULL WHERE victim_name = '木島祥子'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'confrontation', 'at', '05:45', 'place', '植物園内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["kijima-found-missing-tags","kijima-confronted-narahara"]'), 'kind', 'claim'), json_object('id', 'narahara-leaves', 'at', '05:52', 'place', '東温室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-left-east-house","manual-water-zero"]'), 'kind', 'solid'), json_object('id', 'auto-misting', 'at', '06:00', 'place', '東温室', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["auto-misting-ran-0600"]'), 'kind', 'solid'), json_object('id', 'corridor-sighting', 'at', '06:05', 'place', '通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '大西蒼太' LIMIT 1)), 'facts', json('["onishi-saw-narahara-0605"]'), 'kind', 'solid'), json_object('id', 'kijima-death', 'at', '06:08', 'place', '標本庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-killed-kijima-0608"]'), 'kind', 'claim'), json_object('id', 'narahara-returns', 'at', '06:15', 'place', '東温室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-returned-east-0615"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '06:30', 'place', '標本庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '水沢浩司' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '大西蒼太' LIMIT 1)), 'facts', json('["body-found-0630"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"narahara-watering-alibi","about":"narahara-left-east-house"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mizusawa-no-pressure","about":"mizusawa-demanded-naming-rights"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '水沢浩司'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"onishi-overtime-clean","about":"onishi-falsified-overtime"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '大西蒼太'; +--> statement-breakpoint +UPDATE evidences SET description = '5時50分から6時20分まで手灌水の使用量はゼロ。6時から6時04分の自動ミストだけが別系統で作動している。', contradicts = json('["lie:narahara-watering-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND label = '東温室の手灌水用水道メーター'; +--> statement-breakpoint +UPDATE evidences SET description = '大西は6時05分ごろ、標本庫へ向かう楢原を見ている。', contradicts = json('["lie:narahara-watering-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND label = '六時五分の標本庫通路の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '消えた挿し穂と楢原の担当日が一覧化され、管理札の欠落も記録されている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND label = '木島の希少植物管理ノート'; +--> statement-breakpoint +UPDATE evidences SET description = '水沢が寄付継続と新温室の企業名表示を結びつけて要求したメール。事件時刻の行動とは関係しない。', contradicts = json('["lie:mizusawa-no-pressure"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND label = '命名権を条件にした寄付メール'; +--> statement-breakpoint +UPDATE evidences SET description = '申請時間の一部に施設内へいなかった記録があり、残業水増しは分かるが事件当朝の目撃とは両立する。', contradicts = json('["lie:onishi-overtime-clean"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND label = '大西の残業申請と入退室記録'; +--> statement-breakpoint +UPDATE scenarios SET title = '青燈社深夜録', victim_found_in = '編集長室', victim_estimated_death_at = NULL WHERE victim_name = '石橋礼司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'royalty-confrontation', 'at', '20:25', 'place', '編集部内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-diverted-royalties","ishibashi-found-royalty-gap","ishibashi-warned-kawase"]'), 'kind', 'claim'), json_object('id', 'shido-argument', 'at', '20:31', 'place', '編集部内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '志堂透' LIMIT 1)), 'facts', json('["shido-argued-2031","shido-plagiarism"]'), 'kind', 'solid'), json_object('id', 'kawase-enters', 'at', '20:38', 'place', '編集長室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-entered-2038"]'), 'kind', 'claim'), json_object('id', 'ishibashi-death', 'at', '20:41', 'place', '編集長室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-killed-ishibashi-2041"]'), 'kind', 'claim'), json_object('id', 'kawase-leaves', 'at', '20:44', 'place', '編集長室前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '藤本圭' LIMIT 1)), 'facts', json('["fujimoto-saw-kawase-2044"]'), 'kind', 'solid'), json_object('id', 'contract-print', 'at', '20:47', 'place', '編集部', 'room', '', 'record', '印刷履歴', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["contract-printed-2047","kawase-forged-signature"]'), 'kind', 'solid'), json_object('id', 'claimed-signing', 'at', '20:50', 'place', '編集部内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["signed-contract-claims-2050"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '21:00', 'place', '編集長室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '藤本圭' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '志堂透' LIMIT 1)), 'facts', json('["body-found-2100"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kawase-signing-alibi","about":"kawase-forged-signature"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"fujimoto-no-leak","about":"fujimoto-leaked-manuscript"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '藤本圭'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"shido-no-argument","about":"shido-argued-2031"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '志堂透'; +--> statement-breakpoint +UPDATE evidences SET description = '問題の契約書は20時47分に川瀬の端末から送信され、その時刻に初めて印刷されている。', contradicts = json('["lie:kawase-signing-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND label = '社内プリンターの二十時四十七分の履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '藤本は20時44分ごろ、編集長室から離れる川瀬を見ている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND label = '二十時四十四分の編集長室前の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '海外出版社からの入金額に対して作家へ報告された金額が少なく、川瀬の管理口座へ差額が残っている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND label = '海外版権の送金記録と支払帳簿'; +--> statement-breakpoint +UPDATE evidences SET description = '複数の表現が一致しており、志堂が石橋と盗用疑惑で揉めていた理由は分かるが、20時34分以降の行動とは結びつかない。', contradicts = json('["lie:shido-no-argument"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND label = '志堂の原稿と絶版小説の比較'; +--> statement-breakpoint +UPDATE evidences SET description = '藤本の端末から社外へ未発表原稿の一部が送られた記録が残るが、石橋の死亡とは無関係である。', contradicts = json('["lie:fujimoto-no-leak"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND label = '未発表原稿の送信履歴'; +--> statement-breakpoint +UPDATE scenarios SET title = '山麓時計博物館', victim_found_in = '修復室', victim_estimated_death_at = NULL WHERE victim_name = '倉橋宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'clock-offset-remains', 'at', '19:36', 'place', '館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '城戸篤' LIMIT 1)), 'facts', json('["master-clock-fast-eleven","gallery-clocks-follow-master","kido-caused-clock-offset"]'), 'kind', 'solid'), json_object('id', 'false-half-past-chime', 'at', '20:19', 'place', '館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '保科悠人' LIMIT 1)), 'facts', json('["half-past-chime-actual-2019","security-clock-accurate"]'), 'kind', 'solid'), json_object('id', 'shiba-kurahashi-talk', 'at', '20:20', 'place', '西回廊', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-spoke-kurahashi-2020"]'), 'kind', 'solid'), json_object('id', 'shiba-leaves-west', 'at', '20:22', 'place', '西回廊', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-left-west-corridor"]'), 'kind', 'claim'), json_object('id', 'asakura-sighting', 'at', '20:24', 'place', '北廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1)), 'facts', json('["asakura-saw-shiba-2024"]'), 'kind', 'solid'), json_object('id', 'kurahashi-death', 'at', '20:28', 'place', '修復室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-killed-kurahashi"]'), 'kind', 'claim'), json_object('id', 'shiba-return', 'at', '20:34', 'place', '展示準備室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-returned-gallery"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '20:42', 'place', '修復室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '保科悠人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '城戸篤' LIMIT 1)), 'facts', json('["body-found-2042"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"shiba-late-last-seen","about":"shiba-spoke-kurahashi-2020"},{"id":"shiba-stayed-west","about":"shiba-left-west-corridor"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"asakura-no-cash-error","about":"asakura-hid-cash-error"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hoshina-no-image-leak","about":"hoshina-sold-catalog-images"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '保科悠人'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kido-clock-accurate","about":"kido-caused-clock-offset"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '城戸篤'; +--> statement-breakpoint +UPDATE evidences SET description = '防災端末が20時19分を記録した瞬間の監視画像で、中央ホールの親時計は20時30分を示している。両系統には十一分の差がある。', contradicts = json('["lie:kido-clock-accurate","lie:shiba-late-last-seen"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '親時計と防災端末の時刻差'; +--> statement-breakpoint +UPDATE evidences SET description = '朝倉は半時の鐘から約五分後、修復室へ続く北廊下で志波を見ている。鐘の実時刻を補正すると20時24分ごろになる。', contradicts = json('["lie:shiba-stayed-west"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '半時の鐘から五分後の志波'; +--> statement-breakpoint +UPDATE evidences SET description = '志波が登録した著名工房の来歴と原資料が一致せず、倉橋が翌朝の展示撤去と理事会報告を予定していたことが分かる。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '倉橋の来歴照合メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '未公開収蔵品の画像が保科の端末から外部へ送られていたことが分かるが、修復室の事件とは独立している。', contradicts = json('["lie:hoshina-no-image-leak"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '保科の画像送信履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '売上金不足は朝倉自身の二重計上によるものと分かるが、倉橋の死亡とは関係がない。', contradicts = json('["lie:asakura-no-cash-error"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '朝倉の売上金再集計表'; +--> statement-breakpoint +UPDATE evidences SET description = '親時計の補正値を誤った可能性を示す途中メモが残っており、城戸が時刻ずれを把握しながら確認を後回しにしたことが分かる。', contradicts = json('["lie:kido-clock-accurate"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '城戸の同期試験作業票'; +--> statement-breakpoint +UPDATE scenarios SET title = '崖上ホテル', victim_found_in = '執務室', victim_estimated_death_at = '21:05' WHERE victim_name = '長峰宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'invoice-confrontation', 'at', '20:52', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["renovation-shortage","ayase-falsified-invoices","nagamine-called-ayase"]'), 'kind', 'claim'), json_object('id', 'nagamine-death', 'at', '21:05', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["ayase-killed-nagamine"]'), 'kind', 'claim'), json_object('id', 'coat-missing', 'at', '21:18', 'place', 'フロント裏', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '星野結' LIMIT 1)), 'facts', json('["hoshino-saw-empty-coat-hook"]'), 'kind', 'solid'), json_object('id', 'silhouette-staged', 'at', '21:23', 'place', 'ホテル内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["ayase-staged-silhouette","desk-lamp-left-on","stand-feet-dust-mark"]'), 'kind', 'solid'), json_object('id', 'silhouette-seen', 'at', '21:30', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '神田理一' LIMIT 1)), 'facts', json('["kanda-saw-silhouette","kanda-did-not-hear-voice"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:00', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '星野結' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '神田理一' LIMIT 1)), 'facts', json('["body-found-2200","coat-returned-after-discovery"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"ayase-late-death","about":"ayase-killed-nagamine"},{"id":"ayase-clean-invoices","about":"ayase-falsified-invoices"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kanda-no-copy","about":"kanda-secret-manuscript"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '神田理一'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hoshino-no-side-sale","about":"hoshino-secret-photo"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '星野結'; +--> statement-breakpoint +UPDATE evidences SET description = '神田が確認したのは磨りガラス越しの長い輪郭だけで、顔も声も確認していない。', contradicts = json('["lie:ayase-late-death"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '神田の人影証言の詳細'; +--> statement-breakpoint +UPDATE evidences SET description = '21時18分にはフロント裏から消えていた長峰の長い上着が、発見時には執務室の衣紋掛けに掛かっていた。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '長峰の上着の移動'; +--> statement-breakpoint +UPDATE evidences SET description = '執務室の床には衣紋掛けを窓際から磨りガラスの近くへ動かした跡が残る。', contradicts = json('["lie:ayase-late-death"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '衣紋掛けの移動跡'; +--> statement-breakpoint +UPDATE evidences SET description = '長峰が印を付けた請求書には、綾瀬が処理した項目に説明できない差額がまとまっている。', contradicts = json('["lie:ayase-clean-invoices"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '水増しされた改装請求書'; +--> statement-breakpoint +UPDATE evidences SET description = '神田の鞄から長峰の未発表回想録の複写が見つかるが、死亡時刻の偽装とは関係しない。', contradicts = json('["lie:kanda-no-copy"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '無断複写された回想録'; +--> statement-breakpoint +UPDATE evidences SET description = '星野が契約外の館内写真を出版社へ送る準備をしていた記録。事件の人影偽装とは独立した秘密である。', contradicts = json('["lie:hoshino-no-side-sale"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '契約外写真の送付準備'; +--> statement-breakpoint +UPDATE scenarios SET title = '夕凪駅、終夜', victim_found_in = '駅務室', victim_estimated_death_at = NULL WHERE victim_name = '藤崎正雄'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'refund-confrontation', 'at', '21:55', 'place', '駅構内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["arima-skimmed-refunds","fujisaki-found-refund-gap","fujisaki-warned-arima"]'), 'kind', 'claim'), json_object('id', 'arima-office-corridor', 'at', '22:05', 'place', '駅務室前', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1)), 'facts', json('["arima-entered-office-2205","sawada-saw-arima-2205"]'), 'kind', 'solid'), json_object('id', 'fujisaki-death', 'at', '22:08', 'place', '駅務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["arima-killed-fujisaki-2208"]'), 'kind', 'claim'), json_object('id', 'arima-returns-platform', 'at', '22:12', 'place', 'ホーム', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1)), 'facts', json('["arima-left-office-2212","komori-saw-arima-2212"]'), 'kind', 'solid'), json_object('id', 'last-train-departs', 'at', '22:18', 'place', '駅構内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1)), 'facts', json('["last-train-delayed"]'), 'kind', 'solid'), json_object('id', 'certificates-print', 'at', '22:20', 'place', '駅務室', 'room', '', 'record', '印刷履歴', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["certificates-printed-2220"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:25', 'place', '駅務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1)), 'facts', json('["body-found-2225"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"arima-platform-alibi","about":"arima-entered-office-2205"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"komori-clean-disposal","about":"komori-resold-expired-goods"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sawada-stayed-assigned-area","about":"sawada-entered-equipment-room"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮'; +--> statement-breakpoint +UPDATE evidences SET description = '遅延証明書は22時20分に一括印刷されており、22時02分から22時18分の間にはまだ紙として存在していない。', contradicts = json('["lie:arima-platform-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '遅延証明書プリンターの履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '沢田は22時05分ごろ、駅務室へ向かう有馬を見ている。', contradicts = json('["lie:arima-platform-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '二十二時五分の駅務室通路の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '小森は22時12分ごろ、駅務室側からホームへ戻る有馬を見ている。', contradicts = json('["lie:arima-platform-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '二十二時十二分の売店前の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '少額返金が繰り返された日時と現金不足が一致し、有馬の担当時間帯へ集中している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '券売機返金処理と現金残高の差'; +--> statement-breakpoint +UPDATE evidences SET description = '小森が廃棄扱いの商品を知人へ売っていたことは分かるが、駅務室の事件時刻とは結びつかない。', contradicts = json('["lie:komori-clean-disposal"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '売店の廃棄記録と帳簿外販売'; +--> statement-breakpoint +UPDATE evidences SET description = '沢田が正式な指示なしに機器室へ入っていたことが分かるが、22時05分の有馬の目撃とは矛盾しない。', contradicts = json('["lie:sawada-stayed-assigned-area"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '信号機器室の入室記録'; +--> statement-breakpoint +UPDATE scenarios SET title = 'エリュシオン砂嵐', victim_found_in = '解析室', victim_estimated_death_at = NULL WHERE victim_name = 'エレナ・ヴァルガ'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'noah-question', 'at', '21:18', 'place', '基地内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1)), 'facts', json('["noah-sent-question-2118"]'), 'kind', 'solid'), json_object('id', 'fraud-confrontation', 'at', '21:25', 'place', '基地内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1)), 'facts', json('["noah-budget-fraud","elena-found-fraud","elena-would-report"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '21:33', 'place', '連絡廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ルイス・オルテガ' LIMIT 1)), 'facts', json('["luis-saw-noah-corridor"]'), 'kind', 'solid'), json_object('id', 'elena-death', 'at', '21:36', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1)), 'facts', json('["noah-killed-elena"]'), 'kind', 'claim'), json_object('id', 'delayed-reply', 'at', '21:38', 'place', '基地内', 'room', '', 'record', '通信記録', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1)), 'facts', json('["earth-reply-arrived-2138","noah-played-arrival-tone","earth-mars-delay"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:50', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ルイス・オルテガ' LIMIT 1)), 'facts', json('["body-found-2150"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"noah-live-conversation","about":"noah-played-arrival-tone"},{"id":"noah-no-fraud","about":"noah-budget-fraud"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"amira-no-private-channel","about":"amira-private-channel"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"luis-no-unlogged-parts","about":"luis-unlogged-part"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ルイス・オルテガ'; +--> statement-breakpoint +UPDATE evidences SET description = '21時38分の返信パケットには21時18分送信の質問IDが紐づき、約20分前の問いへの返答だと分かる。', contradicts = json('["lie:noah-live-conversation"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND label = '惑星間通信のスレッド記録'; +--> statement-breakpoint +UPDATE evidences SET description = 'ルイスの整備端末に21時33分の位置メモがあり、その場所で解析室側から来たノアとすれ違ったと記録されている。', contradicts = json('["lie:noah-live-conversation"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND label = '21時33分の整備メモ'; +--> statement-breakpoint +UPDATE evidences SET description = 'ノア管理の予算だけ用途が合わず、エレナが翌朝の地球送信用フォルダへ証拠をまとめている。', contradicts = json('["lie:noah-no-fraud"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND label = '調達予算の監査ファイル'; +--> statement-breakpoint +UPDATE evidences SET description = 'アミラの私用通信が見つかるが、解析室の事件とは独立している。', contradicts = json('["lie:amira-no-private-channel"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND label = '私用通信の帯域ログ'; +--> statement-breakpoint +UPDATE scenarios SET title = 'レイライン午前零時', victim_found_in = '第2ブース', victim_estimated_death_at = '23:48' WHERE victim_name = '大門修一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'opening-recording', 'at', '23:20', 'place', '局内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1)), 'facts', json('["opening-recorded-2320"]'), 'kind', 'claim'), json_object('id', 'natsume-meets-daimon', 'at', '23:38', 'place', '第2ブース', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["natsume-secret-meeting"]'), 'kind', 'solid'), json_object('id', 'natsume-leaves', 'at', '23:43', 'place', 'ロビー', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["natsume-left-2343"]'), 'kind', 'solid'), json_object('id', 'minobe-enters', 'at', '23:46', 'place', '第2ブース', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["minobe-entered-2346"]'), 'kind', 'claim'), json_object('id', 'daimon-death', 'at', '23:48', 'place', 'ブース', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["daimon-died-2348"]'), 'kind', 'claim'), json_object('id', 'minobe-returns', 'at', '23:50', 'place', '廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1)), 'facts', json('["kuze-saw-minobe-2350"]'), 'kind', 'solid'), json_object('id', 'recording-queued', 'at', '23:53', 'place', '送出室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["minobe-queued-recording"]'), 'kind', 'claim'), json_object('id', 'opening-airs', 'at', '00:00', 'place', '局内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["recorded-opening-aired"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '00:12', 'place', '第2ブース', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["body-found-0012"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"minobe-live-alibi","about":"minobe-queued-recording"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kuze-no-deletion","about":"kuze-deleted-demo"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"natsume-left-early","about":"natsume-secret-meeting"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介'; +--> statement-breakpoint +UPDATE evidences SET description = '午前零時の冒頭素材は23時53分に登録され、時刻指定で自動再生された記録が残る。', contradicts = json('["lie:minobe-live-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND label = '自動送出システムの実行ログ'; +--> statement-breakpoint +UPDATE evidences SET description = '久世は第二収録ブース側から戻ってくる美濃部を23時50分ごろ目撃している。', contradicts = json('["lie:minobe-live-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND label = '23時50分の廊下の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '夏目が23時43分にロビーへ戻ったことが確認できるが、それ以前の数分間はロビーにいない。', contradicts = json('["lie:natsume-left-early"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND label = '一階ロビーの入退室映像'; +--> statement-breakpoint +UPDATE evidences SET description = '公式報告と実際の放送枠に差があり、美濃部の報告水増しを大門が確認していたことが分かる。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND label = '大門が保存したスポンサー報告の比較メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '久世が事件前に私用録音を削除していたことだけが分かり、殺害時刻とは結びつかない。', contradicts = json('["lie:kuze-no-deletion"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND label = '削除された私用テスト音源の履歴'; +--> statement-breakpoint +UPDATE scenarios SET title = '白夜第六基地', victim_found_in = '解析室', victim_estimated_death_at = NULL WHERE victim_name = '牧瀬航'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'sync-failure', 'at', '21:50', 'place', '基地内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '樋口海' LIMIT 1)), 'facts', json('["central-clock-offset","higuchi-hid-sync-failure"]'), 'kind', 'solid'), json_object('id', 'false-alibi-window', 'at', '22:13', 'place', '基地内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1)), 'facts', json('["terminal-uses-central-clock","access-uses-central-clock","observation-log-central-clock","wall-clock-synced","four-times-not-independent"]'), 'kind', 'solid'), json_object('id', 'real-corridor-sighting', 'at', '22:20', 'place', '通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '沢渡直人' LIMIT 1)), 'facts', json('["handheld-clock-correct","sawatari-saw-shinomiya-real-2220","displayed-time-was-2227"]'), 'kind', 'solid'), json_object('id', 'makise-death', 'at', '22:23', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1)), 'facts', json('["makise-death-real-2223","shinomiya-killed-makise"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:10', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '樋口海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '沢渡直人' LIMIT 1)), 'facts', json('["body-found-2310"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"shinomiya-four-clock-alibi","about":"sawatari-saw-shinomiya-real-2220"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"higuchi-no-serious-offset","about":"central-clock-offset"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '樋口海'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sawatari-no-extra-power","about":"sawatari-used-extra-power"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '沢渡直人'; +--> statement-breakpoint +UPDATE evidences SET description = '通信端末、入室履歴、観測ログ、通信区画前の壁時計がすべて同じ中央時刻系を参照していることが分かる。', contradicts = json('["lie:shinomiya-four-clock-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '基地時刻系の接続図'; +--> statement-breakpoint +UPDATE evidences SET description = '21時50分以降、中央時刻系が実際より七分進んだ状態だったことが保守記録から確認できる。', contradicts = json('["lie:higuchi-no-serious-offset","lie:shinomiya-four-clock-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '七分の同期ずれ記録'; +--> statement-breakpoint +UPDATE evidences SET description = '基地時刻系と同期しない携帯時計で、沢渡は22時20分に解析室側の通路で篠宮を見たと記録している。', contradicts = json('["lie:shinomiya-four-clock-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '沢渡の整備時刻メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '牧瀬の会議メモに、篠宮によるデータ除外の妥当性を全員の前で確認する予定が記されている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '翌朝の研究会議議題'; +--> statement-breakpoint +UPDATE evidences SET description = '樋口が同期障害の共有を遅らせていたことが分かるが、牧瀬の死とは独立した隠し事である。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '障害共有の遅延'; +--> statement-breakpoint +UPDATE evidences SET description = '沢渡が許可なく私物機器を基地電源へ接続していたことが分かるが、解析室の事件とは無関係である。', contradicts = json('["lie:sawatari-no-extra-power"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '私物機器の電源使用'; +--> statement-breakpoint +UPDATE scenarios SET title = '白嶺診療所', victim_found_in = '事務室', victim_estimated_death_at = NULL WHERE victim_name = '星名悟'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'badge-loan', 'at', '20:55', 'place', '診療所内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1)), 'facts', json('["kuga-borrowed-badge","sera-approved-loan"]'), 'kind', 'claim'), json_object('id', 'hoshina-death', 'at', '21:08', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1)), 'facts', json('["hoshina-death-2108","sera-killed-hoshina"]'), 'kind', 'claim'), json_object('id', 'orange-passage', 'at', '21:18', 'place', '検査廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '相原誠' LIMIT 1)), 'facts', json('["orange-badge-passed-2118","aihara-saw-orange-suit","person-was-kuga-2118"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '相原誠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sera-badge-proves-hoshina","about":"sera-approved-loan"},{"id":"sera-saw-hoshina-late","about":"hoshina-death-2108"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kuga-used-own-badge","about":"kuga-borrowed-badge"},{"id":"kuga-no-private-print","about":"kuga-printed-private-results"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"aihara-never-left","about":"aihara-left-monitor"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '相原誠'; +--> statement-breakpoint +UPDATE evidences SET description = '20時55分、久我の認証不良により星名の橙色バッジを一時貸与し、世良が了承したと記されている。', contradicts = json('["lie:kuga-used-own-badge","lie:sera-badge-proves-hoshina"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '一時バッジ貸与メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '記録されているのは橙色バッジの通過であり、使用者の顔や氏名を直接確認した記録ではない。', contradicts = json('["lie:sera-badge-proves-hoshina"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '二十一時十八分の認証記録'; +--> statement-breakpoint +UPDATE evidences SET description = '久我は星名の橙色バッジを着けたまま21時18分に検査廊下を通ったと認める。', contradicts = json('["lie:sera-saw-hoshina-late"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '久我の検査廊下通過'; +--> statement-breakpoint +UPDATE evidences SET description = '星名の予定表に、翌朝最初の案件として世良による同意書管理手順の独断変更を本部へ報告すると記されている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '本部報告予定の確認メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '久我が業務外の個人的な検査結果を印刷していたことが分かるが、星名の死とは独立した隠し事である。', contradicts = json('["lie:kuga-no-private-print"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '久我の私用印刷履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '相原が数分間監視席を離れていたことが分かるが、21時18分の目撃自体はその前後に起きている。', contradicts = json('["lie:aihara-never-left"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '警備席の私用電話記録'; +--> statement-breakpoint +UPDATE scenarios SET title = '青雨堂雨譚', victim_found_in = '店奥', victim_estimated_death_at = NULL WHERE victim_name = '水野英治'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kuroda-offer', 'at', '18:28', 'place', '店内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1)), 'facts', json('["kuroda-secret-offer"]'), 'kind', 'claim'), json_object('id', 'swap-discovered', 'at', '18:37', 'place', '店内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["mizuno-discovered-swap"]'), 'kind', 'claim'), json_object('id', 'kuroda-leaves', 'at', '18:42', 'place', '軒下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1)), 'facts', json('["kuroda-left-1842"]'), 'kind', 'solid'), json_object('id', 'kuroda-sighting', 'at', '18:47', 'place', '店内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["kuroda-saw-makino-1847"]'), 'kind', 'solid'), json_object('id', 'kuroda-under-eaves', 'at', '18:47', 'place', '軒下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1)), 'facts', json('["kuroda-saw-makino-1847"]'), 'kind', 'solid'), json_object('id', 'mizuno-death', 'at', '18:50', 'place', '店奥', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["makino-killed-mizuno-1850"]'), 'kind', 'claim'), json_object('id', 'makino-departs', 'at', '18:56', 'place', '店先', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["makino-left-1856","sena-saw-makino-leave"]'), 'kind', 'solid'), json_object('id', 'sena-in-cafe', 'at', '18:56', 'place', '向かいの喫茶', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '瀬名真琴' LIMIT 1)), 'facts', json('["sena-saw-makino-leave"]'), 'kind', 'solid'), json_object('id', 'parcel-posted', 'at', '19:08', 'place', '郵便窓口', 'room', '', 'record', '受付', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["post-receipt-1908"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '19:15', 'place', '店奥', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '瀬名真琴' LIMIT 1)), 'facts', json('["body-found-1915"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"makino-post-office-alibi","about":"makino-left-1856"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kuroda-no-secret-deal","about":"kuroda-secret-offer"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sena-no-debt","about":"sena-owed-money"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '瀬名真琴'; +--> statement-breakpoint +UPDATE evidences SET description = '小包の受付時刻は19時08分。18時台に牧野が窓口にいたことを示す記録はない。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '郵便窓口の十九時八分のレシート'; +--> statement-breakpoint +UPDATE evidences SET description = '黒田は18時47分ごろ、青雨堂の正面ガラス越しに牧野が店内にいるのを見ている。', contradicts = json('["lie:makino-post-office-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '雨宿り中の黒田の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '瀬名は18時56分ごろ、小包を持って青雨堂から出る牧野を見ている。', contradicts = json('["lie:makino-post-office-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '十八時五十六分の喫茶店からの目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '初版本として保管されていた本は精巧な複製で、在庫と発送を扱う者ならすり替えの機会があった。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '店頭に残った複製本'; +--> statement-breakpoint +UPDATE evidences SET description = '黒田が帳簿外の現金取引を提案した金額のメモが残るが、事件時刻の行動とは結びつかない。', contradicts = json('["lie:kuroda-no-secret-deal"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '黒田の現金取引メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '水野が瀬名へ百万円を貸していた記録。返済は遅れているが、強い取り立てをしていた形跡はない。', contradicts = json('["lie:sena-no-debt"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '瀬名への貸付記録'; +--> statement-breakpoint +UPDATE scenarios SET title = '上海河岸倉庫', victim_found_in = '事務室', victim_estimated_death_at = '21:52' WHERE victim_name = '周文海'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'blank-form-signed', 'at', '21:35', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1)), 'facts', json('["zhou-signed-blank-form"]'), 'kind', 'claim'), json_object('id', 'chen-sees-form', 'at', '21:40', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1)), 'facts', json('["chen-saw-blank-time"]'), 'kind', 'solid'), json_object('id', 'zhou-death', 'at', '21:52', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1)), 'facts', json('["lin-killed-zhou","lin-smuggled-silk","zhou-found-smuggling"]'), 'kind', 'claim'), json_object('id', 'time-added', 'at', '22:05', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1)), 'facts', json('["lin-added-2205","top-sheet-time-added-later"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:20', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '王世傑' LIMIT 1)), 'facts', json('["body-found-2220"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"lin-zhou-alive-2205","about":"lin-added-2205"},{"id":"lin-no-smuggling","about":"lin-smuggled-silk"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"chen-no-bribe","about":"chen-bribed-inspector"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"wang-no-cargo-theft","about":"wang-stole-cargo"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '王世傑'; +--> statement-breakpoint +UPDATE evidences SET description = '署名は三枚すべてに複写されているが、22時05分という時刻は上紙にしか存在しない。', contradicts = json('["lie:lin-zhou-alive-2205"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND label = '三枚綴りの搬出伝票'; +--> statement-breakpoint +UPDATE evidences SET description = '陳の作業メモには署名済み伝票の番号と「時刻未記入」と残っている。', contradicts = json('["lie:lin-zhou-alive-2205"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND label = '21時40分の書記メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '倉庫の実数と帳簿が合わず、周が林の担当欄に翌朝再検査の印を付けている。', contradicts = json('["lie:lin-no-smuggling"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND label = '絹荷の不一致'; +--> statement-breakpoint +UPDATE evidences SET description = '王が持ち出そうとしていた破損品が見つかるが、事務室の事件とは独立している。', contradicts = json('["lie:wang-no-cargo-theft"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND label = '隠された破損品'; +--> statement-breakpoint +UPDATE scenarios SET title = '白庭彫刻館', victim_found_in = '離れ展示室', victim_estimated_death_at = NULL WHERE victim_name = '青沼卓'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'snowfall-begins', 'at', '20:10', 'place', '母屋', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["snow-started-2010"]'), 'kind', 'solid'), json_object('id', 'kurata-crosses', 'at', '20:17', 'place', '離れ展示室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1)), 'facts', json('["kurata-crossed-2017"]'), 'kind', 'claim'), json_object('id', 'aoonuma-death', 'at', '20:20', 'place', '離れ展示室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1)), 'facts', json('["aoonuma-death-2020","kurata-killed-aoonuma"]'), 'kind', 'claim'), json_object('id', 'path-cleared', 'at', '20:26', 'place', '屋外通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["anzai-cleared-path-2026","snow-covered-after-clearing"]'), 'kind', 'solid'), json_object('id', 'kurata-back-main', 'at', '20:31', 'place', '母屋裏口', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["anzai-saw-kurata-2031"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:50', 'place', '離れ展示室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '江波涼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["body-found-2050","no-tracks-at-discovery"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kurata-no-tracks-alibi","about":"kurata-crossed-2017"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"anzai-sensor-natural","about":"anzai-broke-sensor"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"enami-model-intact","about":"enami-damaged-model"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '江波涼'; +--> statement-breakpoint +UPDATE evidences SET description = '安西が排水口確認のため母屋から離れまで通路の雪を一度掃き直したことが記録されている。', contradicts = json('["lie:kurata-no-tracks-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '二十時二十六分の除雪作業記録'; +--> statement-breakpoint +UPDATE evidences SET description = '安西は除雪を終えた直後、母屋の裏口付近にいる倉田を見ている。', contradicts = json('["lie:kurata-no-tracks-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '二十時三十一分の裏口目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '発見時の表面は20時26分の除雪後に積もった雪で、それ以前の足跡を保存していないことが分かる。', contradicts = json('["lie:kurata-no-tracks-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '通路の積雪層メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '倉田が担当した複数作品で、根拠資料と合わない来歴修正が行われ、青沼が翌日理事会へ報告する印を付けている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '来歴記録の修正履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '安西の作業ミスでセンサーが壊れたことが分かるが、青沼の死とは独立した隠し事である。', contradicts = json('["lie:anzai-sensor-natural"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '破損した雪害センサー'; +--> statement-breakpoint +UPDATE evidences SET description = '江波が展示前の試作品を壊して隠していたことが分かるが、離れ展示室の事件とは無関係である。', contradicts = json('["lie:enami-model-intact"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '破損した試作品'; +--> statement-breakpoint +UPDATE scenarios SET title = '北岳観測所', victim_found_in = '資料室', victim_estimated_death_at = NULL WHERE victim_name = '神崎遼'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kanzaki-warning', 'at', '21:40', 'place', '観測所内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["kanzaki-found-fabrication","kanzaki-would-retract"]'), 'kind', 'claim'), json_object('id', 'kurose-data-room', 'at', '21:48', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '黒瀬俊介' LIMIT 1)), 'facts', json('["kurose-entered-data-room","kurose-copied-data"]'), 'kind', 'solid'), json_object('id', 'interval-start', 'at', '21:50', 'place', '屋上', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["camera-interval-started","camera-kept-shooting"]'), 'kind', 'solid'), json_object('id', 'hiyama-leaves-roof', 'at', '21:54', 'place', '屋上', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-left-roof-2154"]'), 'kind', 'claim'), json_object('id', 'muroi-sighting', 'at', '21:57', 'place', '廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '室井邦彦' LIMIT 1)), 'facts', json('["muroi-saw-hiyama-2157"]'), 'kind', 'solid'), json_object('id', 'kanzaki-death', 'at', '22:00', 'place', '資料室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-killed-kanzaki-2200"]'), 'kind', 'claim'), json_object('id', 'hiyama-returns', 'at', '22:06', 'place', '屋上', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-returned-roof-2206"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '22:15', 'place', '資料室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '黒瀬俊介' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '室井邦彦' LIMIT 1)), 'facts', json('["body-found-2215"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hiyama-roof-alibi","about":"hiyama-left-roof-2154"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kurose-stayed-room","about":"kurose-entered-data-room"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '黒瀬俊介'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"muroi-no-private-heater","about":"muroi-broke-heater-rule"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '室井邦彦'; +--> statement-breakpoint +UPDATE evidences SET description = '21時50分から三分ごとの自動撮影が設定され、撮影者がシャッターに触れた記録はない。', contradicts = json('["lie:hiyama-roof-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND label = 'カメラのインターバル撮影設定'; +--> statement-breakpoint +UPDATE evidences SET description = '室井は資料室へ向かう廊下で日山とすれ違っている。', contradicts = json('["lie:hiyama-roof-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND label = '二十一時五十七分の廊下の目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '日山の補正値だけが元データと合わず、翌朝の論文撤回を示す神崎の書き込みが残っている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND label = '神崎の観測データ検証メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '21時48分に黒瀬のカードで解析室へ入室した記録があり、未公開データの無断コピーも確認できる。', contradicts = json('["lie:kurose-stayed-room"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND label = '解析室の入室記録'; +--> statement-breakpoint +UPDATE evidences SET description = '室井の私物ヒーターが見つかるが、資料室での事件とは結びつかない。', contradicts = json('["lie:muroi-no-private-heater"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND label = '機械室の私物ヒーター'; +--> statement-breakpoint +UPDATE scenarios SET title = '雪籠りの高原農園', victim_found_in = '事務室', victim_estimated_death_at = '2026-01-15T22:10:00+09:00' WHERE victim_name = '佐久間隆志'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'shortage-discovered', 'at', '21:35', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["audit-shortage-found"]'), 'kind', 'claim'), json_object('id', 'confrontation', 'at', '21:50', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["sakuma-confronted-fuyuki","fuyuki-diverted-sales"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '22:06', 'place', '廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '久世圭太' LIMIT 1)), 'facts', json('["kuze-saw-fuyuki-office-side"]'), 'kind', 'solid'), json_object('id', 'sakuma-death', 'at', '22:10', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["fuyuki-killed-sakuma","fuyuki-left-victim-coat"]'), 'kind', 'claim'), json_object('id', 'staged-chores', 'at', '05:35', 'place', '農園内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["fuyuki-did-morning-chores","feed-board-magnet-moved","fuyuki-boots-wet-straw"]'), 'kind', 'solid'), json_object('id', 'kitchen-light-seen', 'at', '05:48', 'place', '農園内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1)), 'facts', json('["kitchen-light-on","nozomi-assumed-uncle-awake"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '07:10', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '久世圭太' LIMIT 1)), 'facts', json('["body-found-0710"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"fuyuki-slept-through-night","about":"fuyuki-did-morning-chores"},{"id":"fuyuki-no-account-problem","about":"fuyuki-diverted-sales"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"nozomi-no-debt","about":"nozomi-hid-debt"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kuze-no-private-power","about":"kuze-generator-secret"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '久世圭太'; +--> statement-breakpoint +UPDATE evidences SET description = '完了磁石は移動しているが、佐久間が毎朝必ず書く作業時刻の記入がない。作業者を示す仕組みでもない。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '朝飼い作業板'; +--> statement-breakpoint +UPDATE evidences SET description = '六時すぎの冬木の長靴には、外へ出た直後と分かる雪解け水と飼料庫の藁が付いていた。', contradicts = json('["lie:fuyuki-slept-through-night"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '冬木の長靴の雪解け水と藁'; +--> statement-breakpoint +UPDATE evidences SET description = '久世は事務室側から戻ってくる冬木を22時06分ごろに見ている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '二十二時六分の廊下目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '佐久間の手元には不足額と冬木担当分の伝票番号、翌朝再確認する旨のメモが残っている。', contradicts = json('["lie:fuyuki-no-account-problem"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '出荷伝票の不足メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '佐久間が希への個人的な貸付と返済猶予を書き留めた紙。事件の時系列とは結びつかない。', contradicts = json('["lie:nozomi-no-debt"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '希への貸付メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '久世が作業小屋へ無断で電源を引いていたことが分かるが、佐久間の死とは直接結びつかない。', contradicts = json('["lie:kuze-no-private-power"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '予備発電機の私設配線'; +--> statement-breakpoint +UPDATE scenarios SET title = '雪の白環館', victim_found_in = '保存庫', victim_estimated_death_at = NULL WHERE victim_name = '荻原直哉'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'report-confrontation', 'at', '21:02', 'place', '額装作業室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["forged-restoration-report","ogiwara-found-forgery","ogiwara-called-kosaka"]'), 'kind', 'claim'), json_object('id', 'ogiwara-death', 'at', '21:10', 'place', '額装作業室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-killed-ogiwara-framing","framing-paper-fibers"]'), 'kind', 'solid'), json_object('id', 'transfer-to-vault', 'at', '21:18', 'place', '保存庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-moved-ogiwara","cart-used-after-cleaning"]'), 'kind', 'solid'), json_object('id', 'kosaka-exits-vault', 'at', '21:23', 'place', '搬送廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-left-vault-before-lock","kosaka-apron-dust"]'), 'kind', 'solid'), json_object('id', 'vault-seals', 'at', '21:30', 'place', '保存庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '多田圭' LIMIT 1)), 'facts', json('["vault-night-mode","framing-room-open-before-2130","vault-closed-2130"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:05', 'place', '保存庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '朝倉凪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '多田圭' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["body-found-2205"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kosaka-vault-crime","about":"kosaka-killed-ogiwara-framing"},{"id":"kosaka-report-clean","about":"forged-restoration-report"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"asakura-no-private-loan","about":"asakura-secret-private-loan"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '朝倉凪'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"tada-no-extra-break","about":"tada-secret-unlogged-break"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '多田圭'; +--> statement-breakpoint +UPDATE evidences SET description = '保存庫が外から通常操作できなくなったのは21時30分で、それ以前には搬送作業が可能だった。', contradicts = json('["lie:kosaka-vault-crime"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '保存庫の夜間環境管理記録'; +--> statement-breakpoint +UPDATE evidences SET description = '清掃後の額装作業室に、荻原が確認していた作品台紙と同じ紙片が散っている。', contradicts = json('["lie:kosaka-vault-crime"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '額装作業室の台紙片'; +--> statement-breakpoint +UPDATE evidences SET description = '清掃後に所定位置へ戻した台車が21時台に再使用され、額装作業室と保存庫の間を動いた形跡がある。', contradicts = json('["lie:kosaka-vault-crime"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '作品搬送台車の再使用跡'; +--> statement-breakpoint +UPDATE evidences SET description = '香坂の作業着には額装作業室で扱う古い台紙の粉が多く付いている。', contradicts = json('["lie:kosaka-vault-crime"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '香坂の作業着の台紙粉'; +--> statement-breakpoint +UPDATE evidences SET description = '香坂の報告上は使用したことになっている材料が在庫から減っておらず、荻原が確認印を付けている。', contradicts = json('["lie:kosaka-report-clean"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '修復工程と材料在庫の不一致'; +--> statement-breakpoint +UPDATE evidences SET description = '朝倉が所蔵作品を知人の撮影へ無断で貸していた記録。主事件とは独立している。', contradicts = json('["lie:asakura-no-private-loan"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '無断貸し出し記録'; +--> statement-breakpoint +UPDATE evidences SET description = '多田が警備記録に残さず休憩を延ばしていたことが分かるが、保存庫閉鎖の記録とは別問題である。', contradicts = json('["lie:tada-no-extra-break"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '警備記録にない休憩'; +--> statement-breakpoint +UPDATE scenarios SET title = '録音所ノース・レイク', victim_found_in = '編集室', victim_estimated_death_at = NULL WHERE victim_name = '冬木圭介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'roomtone-loop-made', 'at', '22:46', 'place', '第2ブース', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-made-roomtone-loop"]'), 'kind', 'solid'), json_object('id', 'loop-recording-starts', 'at', '22:50', 'place', '第2ブース', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["loop-routed-to-recorder","recording-repeats-identically"]'), 'kind', 'solid'), json_object('id', 'manabe-leaves', 'at', '22:53', 'place', '監視席', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-left-booth"]'), 'kind', 'claim'), json_object('id', 'shirase-hearing', 'at', '22:56', 'place', '第2ブース', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '白瀬環' LIMIT 1)), 'facts', json('["shirase-heard-loop"]'), 'kind', 'solid'), json_object('id', 'makimura-sighting', 'at', '22:58', 'place', '機材廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '牧村葉月' LIMIT 1)), 'facts', json('["makimura-saw-manabe"]'), 'kind', 'solid'), json_object('id', 'fuyuki-death', 'at', '23:02', 'place', '編集室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-killed-fuyuki"]'), 'kind', 'claim'), json_object('id', 'manabe-return', 'at', '23:08', 'place', '監視席', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-returned-booth"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:18', 'place', '編集室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '鷹野徹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '白瀬環' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '牧村葉月' LIMIT 1)), 'facts', json('["body-found-2318"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"manabe-booth-alibi","about":"manabe-left-booth"},{"id":"manabe-live-input","about":"loop-routed-to-recorder"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"shirase-no-leak","about":"shirase-broke-embargo"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '白瀬環'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"takano-no-cost-shift","about":"takano-hid-contract-change"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '鷹野徹'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"makimura-no-damage","about":"makimura-damaged-microphone"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '牧村葉月'; +--> statement-breakpoint +UPDATE evidences SET description = '空調音や小さな物音まで含めた波形が四十七秒周期で完全一致し、同じ室内音が繰り返し再生されていたと分かる。', contradicts = json('["lie:manabe-booth-alibi","lie:manabe-live-input"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '四十七秒ごとに一致する収録波形'; +--> statement-breakpoint +UPDATE evidences SET description = '牧村は22時58分ごろ、編集室へ続く機材廊下で真鍋を見ている。', contradicts = json('["lie:manabe-booth-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '二十二時五十八分の機材廊下目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '真鍋の作業端末から未公開マスターが外部媒体へ複製され、冬木が翌朝のアクセス停止を記したメモを残している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '未公開音源の複製履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '白瀬が公開前の新曲情報を知人へ送っていたことが分かるが、編集室の事件とは独立している。', contradicts = json('["lie:shirase-no-leak"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '白瀬の新曲情報メッセージ'; +--> statement-breakpoint +UPDATE evidences SET description = '鷹野が承認前に制作費項目を付け替えたことが分かるが、冬木の死亡とは別件である。', contradicts = json('["lie:takano-no-cost-shift"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '鷹野の制作費付け替え表'; +--> statement-breakpoint +UPDATE evidences SET description = '牧村が落としたマイクと隠したケースが見つかるが、事件とは無関係の機材事故だった。', contradicts = json('["lie:makimura-no-damage"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '傷のある収録用マイク'; +--> statement-breakpoint +UPDATE scenarios SET title = '雪夜の白燕座', victim_found_in = '演出控室', victim_estimated_death_at = NULL WHERE victim_name = '瀬尾雅人'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'auto-cues-set', 'at', '21:57', 'place', '調光卓', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["cue-console-auto-mode","kunieda-set-auto-cues"]'), 'kind', 'solid'), json_object('id', 'cues-start', 'at', '22:00', 'place', '調光卓', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1)), 'facts', json('["cues-ran-automatically","toba-saw-light-changes"]'), 'kind', 'solid'), json_object('id', 'kunieda-leaves', 'at', '22:03', 'place', '調光卓', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-left-console"]'), 'kind', 'claim'), json_object('id', 'sasai-sighting', 'at', '22:10', 'place', '舞台袖', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '笹井徹' LIMIT 1)), 'facts', json('["sasai-saw-kunieda-2210"]'), 'kind', 'solid'), json_object('id', 'seo-death', 'at', '22:14', 'place', '演出控室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-killed-seo"]'), 'kind', 'claim'), json_object('id', 'kunieda-return', 'at', '22:19', 'place', '調光卓', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-returned-console"]'), 'kind', 'claim'), json_object('id', 'cues-end', 'at', '22:20', 'place', '調光卓', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1)), 'facts', json('["cues-ran-automatically"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:36', 'place', '演出控室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '柊真琴' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '笹井徹' LIMIT 1)), 'facts', json('["body-found-2236"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kunieda-console-alibi","about":"kunieda-left-console"},{"id":"kunieda-no-auto-cues","about":"kunieda-set-auto-cues"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hiiragi-no-script-leak","about":"hiiragi-hid-script-leak"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '柊真琴'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"toba-no-costume-damage","about":"toba-hid-costume-damage"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sasai-no-skipped-check","about":"sasai-bypassed-inspection"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '笹井徹'; +--> statement-breakpoint +UPDATE evidences SET description = '21時57分に二十分間の自動進行が設定され、22時00分から20分まで手動キュー入力なしで照明が切り替わっている。', contradicts = json('["lie:kunieda-console-alibi","lie:kunieda-no-auto-cues"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '調光卓の自動進行履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '笹井は22時10分ごろ、演出控室へ続く舞台袖通路で国枝を見ている。', contradicts = json('["lie:kunieda-console-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '二十二時十分の舞台袖目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '国枝の請求に実在しないスタッフ名が複数含まれ、瀬尾が翌朝の運営会社報告と進行担当変更を記している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '架空スタッフを含む残業費一覧'; +--> statement-breakpoint +UPDATE evidences SET description = '柊が公開前の改稿台本を知人へ送っていたことが分かるが、演出控室の事件とは独立している。', contradicts = json('["lie:hiiragi-no-script-leak"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '柊の改稿台本送信履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '鳥羽が高価な衣装を傷め、自分で補修して報告しなかったことが分かるが、事件とは別件である。', contradicts = json('["lie:toba-no-costume-damage"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '鳥羽が隠した衣装補修'; +--> statement-breakpoint +UPDATE evidences SET description = '笹井が舞台機構の点検を一項目省略していたことが分かるが、瀬尾の死亡とは無関係である。', contradicts = json('["lie:sasai-no-skipped-check"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '笹井の点検省略記録'; +--> statement-breakpoint +UPDATE scenarios SET title = '海浜水族館、閉館後', victim_found_in = '検疫準備室', victim_estimated_death_at = NULL WHERE victim_name = '江波慎吾'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'feeder-set', 'at', '23:51', 'place', '給餌台', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["feeder-auto-capable","feeding-lamp-motor-linked","morishita-set-auto-feeder"]'), 'kind', 'solid'), json_object('id', 'first-cycle', 'at', '23:55', 'place', '展示側', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["feeder-cycled"]'), 'kind', 'solid'), json_object('id', 'morishita-leaves', 'at', '23:57', 'place', '給餌台', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-left-station"]'), 'kind', 'claim'), json_object('id', 'sagara-sighting', 'at', '00:02', 'place', '通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '相良芳江' LIMIT 1)), 'facts', json('["sagara-saw-morishita"]'), 'kind', 'solid'), json_object('id', 'enami-death', 'at', '00:05', 'place', '検疫準備室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-killed-enami"]'), 'kind', 'claim'), json_object('id', 'morishita-return', 'at', '00:09', 'place', '給餌台', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-returned"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '00:12', 'place', '検疫準備室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '御子柴徹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '榊原直' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '相良芳江' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"morishita-feeding-alibi","about":"morishita-left-station"},{"id":"morishita-no-auto","about":"morishita-set-auto-feeder"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sakakibara-no-stock-edit","about":"sakakibara-hid-stock"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '榊原直'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mikoshiba-no-camera","about":"mikoshiba-private-camera"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '御子柴徹'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sagara-no-skip","about":"sagara-skipped-round"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '相良芳江'; +--> statement-breakpoint +UPDATE evidences SET description = '23時51分に四分間隔の自動運転へ切り替えられ、指定間隔で給餌装置と青い灯りが動作している。', contradicts = json('["lie:morishita-feeding-alibi","lie:morishita-no-auto"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '深海水槽の自動給餌設定'; +--> statement-breakpoint +UPDATE evidences SET description = '相良は00時02分ごろ、検疫準備室へ続く通路で森下とすれ違っている。', contradicts = json('["lie:morishita-feeding-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '零時二分のバックヤード目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '森下の担当水槽だけ過去の記録と当日の記録で数値が不自然に変わり、江波が翌朝の報告予定を書き残している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '飼育記録の版差分'; +--> statement-breakpoint +UPDATE evidences SET description = '榊原が処分予定在庫の記録を後から修正していたことが分かるが、検疫準備室の事件とは別件である。', contradicts = json('["lie:sakakibara-no-stock-edit"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '榊原の在庫修正履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '水槽内から御子柴の私物カメラが見つかるが、検疫準備室の事件とは結びつかない。', contradicts = json('["lie:mikoshiba-no-camera"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '御子柴の私物カメラ'; +--> statement-breakpoint +UPDATE evidences SET description = '23時45分の西展示区画の巡回記録だけ位置確認がなく、相良が休憩していたことが分かる。', contradicts = json('["lie:sagara-no-skip"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '相良の巡回抜け'; +--> statement-breakpoint +UPDATE scenarios SET title = '豪雨の発電所', victim_found_in = '旧制御室', victim_estimated_death_at = NULL WHERE victim_name = '峰岸達也'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'records-confrontation', 'at', '20:12', 'place', '旧制御室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-falsified-inspection","minegishi-found-falsification","minegishi-called-kido"]'), 'kind', 'claim'), json_object('id', 'walkway-still-open', 'at', '20:15', 'place', '保守歩廊', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["walkway-passable-2015","old-walkway-connects"]'), 'kind', 'solid'), json_object('id', 'kido-enters', 'at', '20:18', 'place', '旧制御室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-used-walkway"]'), 'kind', 'claim'), json_object('id', 'minegishi-death', 'at', '20:24', 'place', '旧制御室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-killed-minegishi"]'), 'kind', 'claim'), json_object('id', 'kido-leaves', 'at', '20:29', 'place', '保守歩廊', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-left-before-rise","kido-boot-silt"]'), 'kind', 'solid'), json_object('id', 'water-cuts-route', 'at', '20:40', 'place', '保守歩廊', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["walkway-closed-2040","water-log-recorded-rise"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:10', 'place', '旧制御室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '西園寺悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["front-door-remained-locked","body-found-2110"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kido-impossible-entry","about":"kido-used-walkway"},{"id":"kido-inspection-complete","about":"kido-falsified-inspection"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"saionji-no-leak","about":"saionji-secret-draft"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '西園寺悠'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"taniguchi-no-bypass","about":"taniguchi-secret-bypass"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航'; +--> statement-breakpoint +UPDATE evidences SET description = '20時15分時点では歩廊を使える水位で、20時40分ごろに通行不能へ変化したことが分かる。', contradicts = json('["lie:kido-impossible-entry"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '排水区画の水位記録'; +--> statement-breakpoint +UPDATE evidences SET description = '正面扉とは別に、排水区画側から旧制御室へ接続する保守歩廊がある。', contradicts = json('["lie:kido-impossible-entry"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '旧保守歩廊の接続図'; +--> statement-breakpoint +UPDATE evidences SET description = '城戸の作業靴には旧保守歩廊周辺に特徴的な堆積泥が付いている。', contradicts = json('["lie:kido-impossible-entry"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '城戸の作業靴の赤褐色泥'; +--> statement-breakpoint +UPDATE evidences SET description = '実施済みの印がある項目に対応する現場記録がなく、峰岸が城戸の欄へ確認印を付けている。', contradicts = json('["lie:kido-inspection-complete"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '未実施箇所の点検票'; +--> statement-breakpoint +UPDATE evidences SET description = '西園寺が監査報告の草案を外部へ送った記録。密室の成立時刻とは関係しない。', contradicts = json('["lie:saionji-no-leak"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '監査報告の外部送付記録'; +--> statement-breakpoint +UPDATE evidences SET description = '谷口が軽微な警報を一時的に非表示にしていた履歴。犯行経路とは独立した規則違反である。', contradicts = json('["lie:taniguchi-no-bypass"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '非表示にされた警報履歴'; +--> statement-breakpoint +UPDATE scenarios SET title = '孤島の青凪荘', victim_found_in = '執務室', victim_estimated_death_at = '21:28' WHERE victim_name = '榊原宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'mail-drafted', 'at', '18:11', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["mail-drafted-1811"]'), 'kind', 'solid'), json_object('id', 'mail-scheduled', 'at', '18:12', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["mail-scheduled-2142","aizawa-knew-scheduled-mail"]'), 'kind', 'solid'), json_object('id', 'aizawa-enters-office', 'at', '21:24', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["aizawa-entered-office-2124"]'), 'kind', 'claim'), json_object('id', 'sakakibara-death', 'at', '21:28', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["aizawa-killed-sakakibara"]'), 'kind', 'claim'), json_object('id', 'horie-sighting', 'at', '21:29', 'place', '廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '堀江充' LIMIT 1)), 'facts', json('["horie-saw-aizawa-2129"]'), 'kind', 'solid'), json_object('id', 'aizawa-joins-hatori', 'at', '21:35', 'place', '講義室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '羽鳥栞' LIMIT 1)), 'facts', json('["aizawa-joined-hatori-2135","aizawa-with-hatori-until-2155"]'), 'kind', 'solid'), json_object('id', 'scheduled-mail-sends', 'at', '21:42', 'place', '執務室', 'room', '', 'record', '送信記録', 'participants', json_array(), 'facts', json('["mail-sent-automatically"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:02', 'place', '執務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '御影崇' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '羽鳥栞' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '堀江充' LIMIT 1)), 'facts', json('["body-found-2202"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"aizawa-mail-means-alive","about":"mail-sent-automatically"},{"id":"aizawa-no-office-visit","about":"aizawa-entered-office-2124"},{"id":"aizawa-did-not-know-schedule","about":"aizawa-knew-scheduled-mail"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mikage-no-budget-shift","about":"mikage-hid-expense-shift"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '御影崇'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hatori-no-material-share","about":"hatori-shared-materials"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '羽鳥栞'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"horie-no-private-room","about":"horie-hid-room-use"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '堀江充'; +--> statement-breakpoint +UPDATE evidences SET description = 'メール本文は18時11分作成、18時12分に21時42分の予定送信へ設定されており、21時42分には端末操作なしで送信されている。', contradicts = json('["lie:aizawa-mail-means-alive","lie:aizawa-did-not-know-schedule"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '二十一時四十二分メールの予定送信情報'; +--> statement-breakpoint +UPDATE evidences SET description = '堀江は21時29分ごろ、榊原の執務室前の廊下から出てくる相沢を見ている。', contradicts = json('["lie:aizawa-no-office-visit"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '二十一時二十九分の執務室前目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '相沢が処理した複数の小口支出に不自然な重複があり、榊原が翌朝の監査提出と担当変更を記している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '相沢の小口支出重複一覧'; +--> statement-breakpoint +UPDATE evidences SET description = '御影が予算超過を隠すため研修費を別年度へ付け替えていたことが分かるが、榊原の死亡とは独立している。', contradicts = json('["lie:mikage-no-budget-shift"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '御影の年度付け替え表'; +--> statement-breakpoint +UPDATE evidences SET description = '羽鳥が契約上未公開の教材を別講座で先に使用していたことが分かるが、事件とは無関係である。', contradicts = json('["lie:hatori-no-material-share"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '羽鳥の未公開教材利用履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '堀江が規則に反して空き客室を私物置き場にしていたことが分かるが、執務室の事件とは別件である。', contradicts = json('["lie:horie-no-private-room"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '空き客室の私物荷物'; +--> statement-breakpoint +UPDATE scenarios SET title = '夕凪灯台', victim_found_in = '整備室', victim_estimated_death_at = NULL WHERE victim_name = '倉橋徹'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'west-seal', 'at', '18:40', 'place', '西外扉', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透' LIMIT 1)), 'facts', json('["west-gate-sealed"]'), 'kind', 'solid'), json_object('id', 'east-round', 'at', '19:25', 'place', '東側', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1)), 'facts', json('["torigoe-coat-wet-before"]'), 'kind', 'claim'), json_object('id', 'panel-reading', 'at', '20:12', 'place', '制御盤', 'room', '', 'record', '計測記録', 'participants', json_array(), 'facts', json('["slate-entry-2012","slate-written-after-round","torigoe-copied-panel-reading"]'), 'kind', 'solid'), json_object('id', 'stair-sighting', 'at', '20:21', 'place', '内階段', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '橋場圭' LIMIT 1)), 'facts', json('["hashiba-saw-torigoe-2021"]'), 'kind', 'solid'), json_object('id', 'kurahashi-death', 'at', '20:24', 'place', '灯台内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1)), 'facts', json('["kurahashi-death-2024","torigoe-killed-kurahashi"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '20:50', 'place', '整備室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '橋場圭' LIMIT 1)), 'facts', json('["body-found-2050"]'), 'kind', 'solid'), json_object('id', 'west-seal-checked', 'at', '20:50', 'place', '西外扉', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透' LIMIT 1)), 'facts', json('["west-seal-intact"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"torigoe-west-round","about":"west-seal-intact"},{"id":"torigoe-not-stairs","about":"hashiba-saw-torigoe-2021"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hashiba-only-work-radio","about":"hashiba-private-channel"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '橋場圭'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"morishita-awake","about":"morishita-slept-on-duty"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透'; +--> statement-breakpoint +UPDATE evidences SET description = '18時40分に封鎖された確認票が事件後も切れておらず、20時台に西側へ出たという説明と両立しない。', contradicts = json('["lie:torigoe-west-round"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '西側外扉の封鎖確認票'; +--> statement-breakpoint +UPDATE evidences SET description = '点検時刻は現場で書くのではなく、巡回後に制御盤の表示やメモを見てまとめて転記する運用だった。', contradicts = json('["lie:torigoe-west-round"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '当直板の記入手順'; +--> statement-breakpoint +UPDATE evidences SET description = '鳥越の外套は19時台の東側巡回で既に濡れており、20時台に西側へ出た証明にはならない。', contradicts = json('["lie:torigoe-west-round"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '外套の先行使用記録'; +--> statement-breakpoint +UPDATE evidences SET description = '橋場は通信時計を確認した直後、整備室近くの内階段で鳥越とすれ違っている。', contradicts = json('["lie:torigoe-not-stairs","lie:torigoe-west-round"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '二十時二十一分の内階段目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '倉橋の翌日提出予定資料に、鳥越の担当分だけ燃料在庫の差を本部へ報告する注記がある。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '補給報告の在庫差メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '橋場が個人的な受信をしていたことが分かるが、整備室の事件とは結びつかない。', contradicts = json('["lie:hashiba-only-work-radio"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '規定外の受信履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '森下が短時間眠っていたことが分かるが、西側外扉の封鎖確認や鳥越の位置とは独立している。', contradicts = json('["lie:morishita-awake"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '発電室の巡回空白'; +--> statement-breakpoint +UPDATE scenarios SET title = '梢庵夜話', victim_found_in = '帳場奥', victim_estimated_death_at = '21:08' WHERE victim_name = '桐谷宗介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'supply-confrontation', 'at', '20:58', 'place', '山宿内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["supplier-kickback","kiritani-found-kickback","kiritani-confronted-akiwa"]'), 'kind', 'claim'), json_object('id', 'kiritani-death', 'at', '21:08', 'place', '帳場奥', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["akiwa-killed-kiritani","akiwa-took-cane"]'), 'kind', 'claim'), json_object('id', 'tapping-staged', 'at', '21:25', 'place', '旧階段', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["akiwa-made-tapping","morisaki-heard-taps","morisaki-heard-no-steps"]'), 'kind', 'solid'), json_object('id', 'morisaki-hears-tapping', 'at', '21:25', 'place', '二階廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '森崎透' LIMIT 1)), 'facts', json('["morisaki-heard-taps","morisaki-heard-no-steps"]'), 'kind', 'solid'), json_object('id', 'cane-hidden', 'at', '21:31', 'place', '厨房脇', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["cane-found-kitchen","rail-fresh-marks"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:50', 'place', '帳場奥', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '榊原蓮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '森崎透' LIMIT 1)), 'facts', json('["body-found-2150"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"akiwa-cane-proves-alive","about":"akiwa-made-tapping"},{"id":"akiwa-clean-supplies","about":"supplier-kickback"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"morisaki-paid-all","about":"morisaki-secret-unpaid"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '森崎透'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sakakibara-no-materials","about":"sakakibara-secret-materials"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '榊原蓮'; +--> statement-breakpoint +UPDATE evidences SET description = '森崎は特徴的な金属音だけを聞き、桐谷の足音や声は確認していない。', contradicts = json('["lie:akiwa-cane-proves-alive"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '森崎の音の聞き分け'; +--> statement-breakpoint +UPDATE evidences SET description = '二つの区画は同じ古い中空隔壁に接し、旧階段側の硬い音が二階から聞こえることがある。', contradicts = json('["lie:akiwa-cane-proves-alive"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '旧階段と二階廊下の中空壁'; +--> statement-breakpoint +UPDATE evidences SET description = '桐谷の杖は発見直前、本人のいる帳場奥ではなく厨房脇の物入れで見つかった。', contradicts = json('["lie:akiwa-cane-proves-alive"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '厨房脇で見つかった杖'; +--> statement-breakpoint +UPDATE evidences SET description = '木製隔壁には杖の金属部分と高さの合う新しい打痕が複数残っている。', contradicts = json('["lie:akiwa-cane-proves-alive"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '旧階段の新しい打痕'; +--> statement-breakpoint +UPDATE evidences SET description = '同じ業者から相場より高い価格で仕入れた記録と、秋庭が処理した伝票がまとまっている。', contradicts = json('["lie:akiwa-clean-supplies"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '不自然な仕入れ伝票'; +--> statement-breakpoint +UPDATE evidences SET description = '森崎には以前からの宿泊代の未払いがあるが、杖音の偽装とは結びつかない。', contradicts = json('["lie:morisaki-paid-all"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '森崎の未払い宿泊票'; +--> statement-breakpoint +UPDATE evidences SET description = '榊原が余剰木材を無断で持ち帰ろうとしていたことが分かるが、事件とは独立した秘密である。', contradicts = json('["lie:sakakibara-no-materials"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '持ち出し予定の余剰木材'; +--> statement-breakpoint +UPDATE scenarios SET title = '星見ヶ丘天象館', victim_found_in = '投影準備室', victim_estimated_death_at = NULL WHERE victim_name = '犬塚誠'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'inuzuka-death', 'at', '21:24', 'place', '投影準備室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1)), 'facts', json('["inuzuka-death-2124","onodera-killed-inuzuka"]'), 'kind', 'claim'), json_object('id', 'booth-lights-down', 'at', '21:30', 'place', '投影室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["booth-dark-2130","glass-reflects-dark-booth"]'), 'kind', 'solid'), json_object('id', 'reflected-sighting', 'at', '21:35', 'place', '天象館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["onodera-behind-matsuda-2135","matsuda-saw-white-figure","figure-was-reflection","onodera-endorsed-sighting"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:00', 'place', '投影準備室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '朝倉葉月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["body-found-2200"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"onodera-sighting-was-inuzuka","about":"figure-was-reflection"},{"id":"onodera-not-behind-matsuda","about":"onodera-behind-matsuda-2135"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"matsuda-full-test","about":"matsuda-hid-test-failure"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"asakura-stock-clean","about":"asakura-hid-stock-loss"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '朝倉葉月'; +--> statement-breakpoint +UPDATE evidences SET description = '投影室を暗くして廊下を明るくすると、観察ガラスには室内より廊下側の人物が強く反射する。', contradicts = json('["lie:onodera-sighting-was-inuzuka"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '観察ガラスの照明条件テスト'; +--> statement-breakpoint +UPDATE evidences SET description = '犬塚だけでなく小野寺も同型の白い展示用上着を着ていたことが確認できる。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '閉館作業時の白い上着'; +--> statement-breakpoint +UPDATE evidences SET description = '松田は白い像を見た直後、数歩後ろにいた小野寺から返事を受けており、反射像の位置関係と一致する。', contradicts = json('["lie:onodera-not-behind-matsuda","lie:onodera-sighting-was-inuzuka"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '松田の立ち位置の記憶'; +--> statement-breakpoint +UPDATE evidences SET description = '犬塚の端末に、小野寺による正式承認のない予算振替を翌朝報告する草案が残っている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '運営法人への予算報告草案'; +--> statement-breakpoint +UPDATE evidences SET description = '松田が事前テストを一部省略していたことが分かるが、犬塚の死とは独立した隠し事である。', contradicts = json('["lie:matsuda-full-test"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '省略された投影機器テスト'; +--> statement-breakpoint +UPDATE evidences SET description = '朝倉が在庫不足を自費補填して隠していたことが分かるが、投影準備室の事件とは無関係である。', contradicts = json('["lie:asakura-stock-clean"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '売店在庫の不足記録'; +--> statement-breakpoint +UPDATE scenarios SET title = '調査船みなも', victim_found_in = '解析室', victim_estimated_death_at = NULL WHERE victim_name = '瀬尾俊'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'launch-loss', 'at', '19:42', 'place', '甲板', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["launch-lost-1942","kariya-saw-launch-loss"]'), 'kind', 'solid'), json_object('id', 'launch-note', 'at', '19:48', 'place', '甲板', 'room', '', 'record', '甲板メモ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["launch-note-signed"]'), 'kind', 'solid'), json_object('id', 'seo-call', 'at', '19:56', 'place', '船内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '野々村岳' LIMIT 1)), 'facts', json('["seo-alive-1956","launch-gone-before-death"]'), 'kind', 'solid'), json_object('id', 'kariya-analysis', 'at', '20:14', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1)), 'facts', json('["kariya-entered-analysis-2014"]'), 'kind', 'claim'), json_object('id', 'seo-death', 'at', '20:18', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1)), 'facts', json('["seo-death-2018","kariya-killed-seo"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '20:22', 'place', '通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["mizuki-saw-kariya-2022"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:40', 'place', '解析室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '野々村岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["body-found-2040"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kariya-outsider-launch","about":"launch-gone-before-death"},{"id":"kariya-no-analysis-room","about":"mizuki-saw-kariya-2022"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mizuki-maintenance-current","about":"mizuki-hid-maintenance-delay"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"nonomura-work-only","about":"nonomura-private-message"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '野々村岳'; +--> statement-breakpoint +UPDATE evidences SET description = '小型作業艇が既に失われたことと、確認者として水城と狩谷の署名が残っている。', contradicts = json('["lie:kariya-outsider-launch"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '十九時四十八分の甲板異常メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '作業艇消失後の19時56分、野々村が瀬尾本人と研究データの確認をしている。', contradicts = json('["lie:kariya-outsider-launch"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '十九時五十六分の船内通話記録'; +--> statement-breakpoint +UPDATE evidences SET description = '水城は解析室側の通路から戻る狩谷を20時22分ごろに目撃している。', contradicts = json('["lie:kariya-no-analysis-room"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '二十時二十二分の通路目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '狩谷が共同研究データを単独成果として外部提出しようとし、瀬尾がその経緯を問題視していたことが分かる。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '共同研究データの提出履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '水城が点検延期を報告していなかったことが分かるが、瀬尾の死とは独立した隠し事である。', contradicts = json('["lie:mizuki-maintenance-current"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '延期された甲板設備点検'; +--> statement-breakpoint +UPDATE evidences SET description = '野々村が勤務中に個人的な通信をしていたことが分かるが、解析室の事件とは無関係である。', contradicts = json('["lie:nonomura-work-only"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '野々村の私的通信'; +--> statement-breakpoint +UPDATE scenarios SET title = '山中研究会館', victim_found_in = '西側事務室', victim_estimated_death_at = NULL WHERE victim_name = '鷺沢修'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'submission-confrontation', 'at', '20:58', 'place', '会館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1)), 'facts', json('["kuga-stole-results","sagisawa-found-submission","sagisawa-confronted-kuga"]'), 'kind', 'claim'), json_object('id', 'sagisawa-death', 'at', '21:10', 'place', '西側事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1)), 'facts', json('["kuga-killed-sagisawa"]'), 'kind', 'claim'), json_object('id', 'west-corridor-sighting', 'at', '21:16', 'place', '西廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["shiraishi-saw-kuga-west"]'), 'kind', 'solid'), json_object('id', 'rumor-starts', 'at', '21:18', 'place', '会館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1)), 'facts', json('["kuga-claimed-saw-east","kuga-started-rumor","no-independent-east-sighting"]'), 'kind', 'claim'), json_object('id', 'rumor-repeated', 'at', '21:22', 'place', '会館内', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["naruse-learned-from-kuga","naruse-repeated-rumor","shiraishi-learned-from-naruse"]'), 'kind', 'solid'), json_object('id', 'east-room-still-unused', 'at', '21:40', 'place', '東資料室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["east-room-unused"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:45', 'place', '西側事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["body-found-2145"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kuga-east-sighting","about":"kuga-started-rumor"},{"id":"kuga-no-stolen-results","about":"kuga-stole-results"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"naruse-no-leak","about":"naruse-secret-embargo"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"shiraishi-no-data-use","about":"shiraishi-secret-data"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧'; +--> statement-breakpoint +UPDATE evidences SET description = '成瀬の情報源は久我、白石の情報源は成瀬であり、独立した三つの目撃ではない。', contradicts = json('["lie:kuga-east-sighting"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '東資料室情報の伝達経路'; +--> statement-breakpoint +UPDATE evidences SET description = '白石は事件直後の時刻に、西側事務室へ続く廊下から出てくる久我を見ている。', contradicts = json('["lie:kuga-east-sighting"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '二十一時十六分の西廊下目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '東資料室は21時前の片付け状態から机も資料箱も動いておらず、鷺沢がそこで資料を使った形跡がない。', contradicts = json('["lie:kuga-east-sighting"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '手つかずの東資料室'; +--> statement-breakpoint +UPDATE evidences SET description = '久我の端末には共同研究の未発表結果を自分単独の成果として投稿する準備が残り、鷺沢からの取り下げ要求も確認できる。', contradicts = json('["lie:kuga-no-stolen-results"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '単独名義の投稿原稿'; +--> statement-breakpoint +UPDATE evidences SET description = '成瀬が公開前の論文集内容を知人へ送っていた記録。東資料室の目撃連鎖とは独立した秘密である。', contradicts = json('["lie:naruse-no-leak"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '公開前論文集の漏洩'; +--> statement-breakpoint +UPDATE evidences SET description = '白石が研究会の参加者情報を自分の調査に転用していたことが分かるが、主事件とは関係しない。', contradicts = json('["lie:shiraishi-no-data-use"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '参加者データの無断流用'; +--> statement-breakpoint +UPDATE scenarios SET title = '霧岳山頂駅', victim_found_in = '運行事務室', victim_estimated_death_at = NULL WHERE victim_name = '高瀬修司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'tool-case-placed', 'at', '21:09', 'place', '非常搬器', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["cabin-display-weight-based","tool-case-heavy-enough","sudo-left-tool-case"]'), 'kind', 'solid'), json_object('id', 'occupancy-start', 'at', '21:10', 'place', '山頂駅', 'room', '', 'record', '', 'participants', json_array(), 'facts', json('["occupancy-display-on"]'), 'kind', 'solid'), json_object('id', 'sudo-leaves-cabin', 'at', '21:12', 'place', '非常搬器', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-left-cabin-area"]'), 'kind', 'claim'), json_object('id', 'enomoto-sighting', 'at', '21:18', 'place', '職員通路', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '榎本澄' LIMIT 1)), 'facts', json('["enomoto-saw-sudo-2118"]'), 'kind', 'solid'), json_object('id', 'takase-death', 'at', '21:22', 'place', '運行事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-killed-takase"]'), 'kind', 'claim'), json_object('id', 'sudo-return', 'at', '21:28', 'place', '非常搬器', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-returned-cabin"]'), 'kind', 'claim'), json_object('id', 'occupancy-end', 'at', '21:30', 'place', '非常搬器', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["occupancy-display-on"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:38', 'place', '運行事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '長峰礼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '榎本澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '折原壮' LIMIT 1)), 'facts', json('["body-found-2138"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"sudo-cabin-alibi","about":"sudo-left-cabin-area"},{"id":"sudo-no-toolcase-seat","about":"sudo-left-tool-case"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"enomoto-no-wallet","about":"enomoto-kept-lost-wallet"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '榎本澄'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"nagamine-no-bypass","about":"nagamine-bypassed-heater"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '長峰礼'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"orihara-no-restricted-deck","about":"orihara-entered-restricted-deck"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '折原壮'; +--> statement-breakpoint +UPDATE evidences SET description = '『乗員1』は一定以上の荷重で点灯し、保守用工具ケースだけでも同じ表示になる。21時台にはそのケースが点検席へ置かれていた。', contradicts = json('["lie:sudo-cabin-alibi","lie:sudo-no-toolcase-seat"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '非常用搬器の座席重量仕様'; +--> statement-breakpoint +UPDATE evidences SET description = '榎本は21時18分ごろ、運行事務室へ続く職員通路で須藤とすれ違っている。', contradicts = json('["lie:sudo-cabin-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '二十一時十八分の職員通路目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '須藤が実施済みと記した点検の一部に作業履歴がなく、高瀬が翌朝の運転停止と安全管理部への報告を記している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '制動点検記録と作業履歴の不一致'; +--> statement-breakpoint +UPDATE evidences SET description = '売店で拾われた財布が正式な遺失物処理をされず榎本のロッカーに入っていたが、事件とは無関係だった。', contradicts = json('["lie:enomoto-no-wallet"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '榎本のロッカーにある拾得財布'; +--> statement-breakpoint +UPDATE evidences SET description = '長峰が規定外に警報を一時停止していたことが分かるが、運行事務室の事件とは別件である。', contradicts = json('["lie:nagamine-no-bypass"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '融雪ヒーターの警報停止履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '折原が立入禁止の保守デッキへ出て撮影していたことが分かるが、事件時刻の運行事務室とは結びつかない。', contradicts = json('["lie:orihara-no-restricted-deck"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '保守デッキで撮られた写真'; +--> statement-breakpoint +UPDATE scenarios SET title = '月見荘十七回忌', victim_found_in = '書斎', victim_estimated_death_at = '20:15' WHERE victim_name = '高瀬涼子'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'dinner-start', 'at', '19:00', 'place', '食堂', 'room', 'dining', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["dinner-started-1900"]'), 'kind', 'solid'), json_object('id', 'fukagawa-leaves', 'at', '19:15', 'place', '廊下', 'room', 'corridor', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["fukagawa-left-1915","fukagawa-at-phone-booth"]'), 'kind', 'solid'), json_object('id', 'ryoko-to-study', 'at', '19:20', 'place', '書斎', 'room', 'study', 'record', '', 'participants', json_array(), 'facts', json('["ryoko-moved-to-study-1920"]'), 'kind', 'solid'), json_object('id', 'kiryu-argument', 'at', '19:35', 'place', '書斎', 'room', 'study', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["kiryu-argued-with-ryoko-1935"]'), 'kind', 'solid'), json_object('id', 'fukagawa-returns', 'at', '19:45', 'place', '食堂', 'room', 'dining', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["fukagawa-returned-1945"]'), 'kind', 'solid'), json_object('id', 'kiryu-passes-mizuki', 'at', '19:50', 'place', '廊下', 'room', 'corridor', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["kiryu-passed-mizuki-1950"]'), 'kind', 'solid'), json_object('id', 'mizuki-poisons', 'at', '19:50', 'place', '書斎', 'room', 'study', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1)), 'facts', json('["mizuki-poisoned-brandy-1950","mizuki-took-aconite"]'), 'kind', 'claim'), json_object('id', 'mizuki-serves', 'at', '20:00', 'place', '書斎', 'room', 'study', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1)), 'facts', json('["mizuki-carried-brandy-2000","brandy-was-poisoned"]'), 'kind', 'solid'), json_object('id', 'ryoko-drinks', 'at', '20:15', 'place', '書斎', 'room', 'study', 'record', '', 'participants', json_array(), 'facts', json('["ryoko-drank-at-2015"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:30', 'place', '書斎', 'room', 'study', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1)), 'facts', json('["death-found-2030"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"fukagawa-study-alibi","about":"fukagawa-at-phone-booth"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"mizuki-single-visit","about":"mizuki-poisoned-brandy-1950"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kiryu-never-entered-study","about":"kiryu-argued-with-ryoko-1935"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼'; +--> statement-breakpoint +UPDATE evidences SET description = '19時15分から19時45分の間、旅館の外から愛人へ発信した記録が残っている。', contradicts = json('["lie:fukagawa-study-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '深川の携帯電話の発着信履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '19時50分ごろ、手ぶらの美月が書斎へ向かうのを桐生が廊下で見ている。', contradicts = json('["lie:mizuki-single-visit"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '桐生が見た、書斎前の廊下ですれ違った人物'; +--> statement-breakpoint +UPDATE evidences SET description = '研究用に栽培されていた株の記録。持ち出しの管理は緩い。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '旅館裏庭の薬草園とトリカブトの管理記録'; +--> statement-breakpoint +UPDATE evidences SET description = '数ヶ月前の日付で、美月を後継者・遺産の受取人に指定している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '涼子の遺言書に記された後継者指定'; +--> statement-breakpoint +UPDATE evidences SET description = '後継者の項に線が引かれ、余白に書き込みがある。日付はごく最近。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '書き直しかけの遺言書の草案'; +--> statement-breakpoint +UPDATE evidences SET description = '瓶とグラスの双方から毒物の反応が出ている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '書斎に残されたブランデーの瓶とグラス'; +--> statement-breakpoint +UPDATE evidences SET description = '19時35分ごろ、書斎で経営方針を巡って交わされた短い口論。', contradicts = json('["lie:kiryu-never-entered-study"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '涼子と桐生が交わした口論の記憶'; +--> statement-breakpoint +UPDATE scenarios SET title = '湾岸データセンター', victim_found_in = '予備部品庫', victim_estimated_death_at = NULL WHERE victim_name = '真田啓介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'schedule-created', 'at', '21:52', 'place', '監視室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-created-scheduled-job"]'), 'kind', 'solid'), json_object('id', 'door-propped', 'at', '21:55', 'place', '予備部品庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '波多野結' LIMIT 1)), 'facts', json('["parts-door-propped"]'), 'kind', 'solid'), json_object('id', 'kuze-leaves', 'at', '21:58', 'place', '監視室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-left-noc"]'), 'kind', 'claim'), json_object('id', 'scheduled-execution', 'at', '22:00', 'place', '監視室', 'room', '', 'record', '実行ログ', 'participants', json_array(), 'facts', json('["scheduled-jobs-ran"]'), 'kind', 'solid'), json_object('id', 'corridor-sighting', 'at', '22:01', 'place', '東側廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '真壁徹' LIMIT 1)), 'facts', json('["makabe-saw-kuze"]'), 'kind', 'solid'), json_object('id', 'sanada-death', 'at', '22:04', 'place', '予備部品庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-killed-sanada"]'), 'kind', 'claim'), json_object('id', 'kuze-return', 'at', '22:08', 'place', '監視室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-returned-noc"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '22:15', 'place', '予備部品庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '波多野結' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '甲田修' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '真壁徹' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kuze-noc-alibi","about":"kuze-left-noc"},{"id":"kuze-no-scheduler","about":"kuze-created-scheduled-job"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"koda-no-copy","about":"koda-copied-config"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '甲田修'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hatano-no-alarm-stop","about":"hatano-suppressed-alarm"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '波多野結'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"makabe-no-doze","about":"makabe-dozed"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '真壁徹'; +--> statement-breakpoint +UPDATE evidences SET description = '21時52分に作成されたジョブが22時00分、03分、06分に自動で処理を実行している。', contradicts = json('["lie:kuze-noc-alibi","lie:kuze-no-scheduler"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '復旧処理の予約ジョブ履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '真壁は22時01分ごろ、予備部品庫方向へ歩く久世を見ている。', contradicts = json('["lie:kuze-noc-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '二十二時一分の東側廊下目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '搬入作業のため扉が半開きに固定され、カード認証なしでも押して入れる状態だった。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '予備部品庫の搬入用ラッチ'; +--> statement-breakpoint +UPDATE evidences SET description = '請求上は交換済みの機器が在庫に残り、久世の処理した保守費だけ数量が一致しない。真田の監査メモもある。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '交換部品の請求書と在庫表'; +--> statement-breakpoint +UPDATE evidences SET description = '顧客設定が甲田の私物端末へ複製されているが、部品庫の事件とは独立している。', contradicts = json('["lie:koda-no-copy"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '甲田の設定ファイル複製履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '波多野が一系統の温度警報を無断停止していたことが分かるが、事件とは別件である。', contradicts = json('["lie:hatano-no-alarm-stop"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '波多野の温度警報停止履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '21時50分から21時59分まで監視端末に操作がなく、真壁が居眠りしていたことが分かる。', contradicts = json('["lie:makabe-no-doze"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '真壁の監視端末操作空白'; +--> statement-breakpoint +UPDATE scenarios SET title = '洋上風力基地', victim_found_in = '会議室', victim_estimated_death_at = NULL WHERE victim_name = '芳賀俊介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'route-loaded', 'at', '00:57', 'place', '屋上', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-repeat-route-capable","hiiragi-loaded-auto-route"]'), 'kind', 'solid'), json_object('id', 'drone-launch', 'at', '01:00', 'place', '屋上', 'room', '', 'record', '飛行ログ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-flew-auto","route-has-no-manual-input"]'), 'kind', 'solid'), json_object('id', 'hiiragi-leaves-roof', 'at', '01:02', 'place', '屋上', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-left-roof"]'), 'kind', 'claim'), json_object('id', 'saegusa-sighting', 'at', '01:08', 'place', '連絡階段', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '三枝千尋' LIMIT 1)), 'facts', json('["saegusa-saw-hiiragi-0108"]'), 'kind', 'solid'), json_object('id', 'haga-death', 'at', '01:12', 'place', '会議室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-killed-haga"]'), 'kind', 'claim'), json_object('id', 'hiiragi-return', 'at', '01:17', 'place', '屋上', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-returned-roof"]'), 'kind', 'claim'), json_object('id', 'drone-lands', 'at', '01:18', 'place', '屋上', 'room', '', 'record', '飛行ログ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-flew-auto"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '01:29', 'place', '会議室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '国分透' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '三枝千尋' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '波木恵' LIMIT 1)), 'facts', json('["body-found-0129"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"hiiragi-roof-alibi","about":"hiiragi-left-roof"},{"id":"hiiragi-manual-flight","about":"hiiragi-loaded-auto-route"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"saegusa-no-reset","about":"saegusa-unauthorized-reset"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '三枝千尋'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"nami-no-edit","about":"nami-edited-weather-note"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '波木恵'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"kokubu-no-stock-shift","about":"kokubu-hid-spare-parts"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '国分透'; +--> statement-breakpoint +UPDATE evidences SET description = '00時57分に反復点検ルートが読み込まれ、01時00分から16分まで手動入力なしの自律飛行状態が続いている。', contradicts = json('["lie:hiiragi-roof-alibi","lie:hiiragi-manual-flight"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '点検ドローンの飛行モード履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '三枝は01時08分ごろ、会議室へ続く連絡階段で柊木を目撃している。', contradicts = json('["lie:hiiragi-roof-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '一時八分の連絡階段目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '柊木が実施済みとした複数の点検写真が過去の画像と一致し、芳賀が翌朝の安全部報告と資格停止を記している。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '過去点検写真との一致'; +--> statement-breakpoint +UPDATE evidences SET description = '三枝が正式承認なく保護装置を一度リセットしていたことが分かるが、会議室の事件とは独立している。', contradicts = json('["lie:saegusa-no-reset"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '三枝の保護装置リセット履歴'; +--> statement-breakpoint +UPDATE evidences SET description = '波木が手書きの風速値を後から修正していたことが分かるが、事件とは無関係の入力ミスだった。', contradicts = json('["lie:nami-no-edit"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '波木の風速記録修正跡'; +--> statement-breakpoint +UPDATE evidences SET description = '国分が別案件の部品を棚卸し在庫へ一時付け替えていたことが分かるが、芳賀の死亡とは別件である。', contradicts = json('["lie:kokubu-no-stock-shift"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '国分の予備部品付け替え表'; +--> statement-breakpoint +UPDATE scenarios SET title = 'ラッザレットの夕映え', victim_found_in = '記録室', victim_estimated_death_at = NULL WHERE victim_name = 'ロレンツォ・ヴァーレ'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'lorenzo-audit', 'at', '18:35', 'place', '検疫島', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-diverted-medicine","lorenzo-found-shortage","lorenzo-confronted-marta"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '18:41', 'place', '廊下', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ニコロ・フェッリ' LIMIT 1)), 'facts', json('["nicolo-saw-marta-before-bell"]'), 'kind', 'solid'), json_object('id', 'lorenzo-death', 'at', '18:44', 'place', '記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-killed-lorenzo"]'), 'kind', 'claim'), json_object('id', 'sunset-bell', 'at', '18:47', 'place', '鐘楼', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ピエトロ・サルヴィ' LIMIT 1)), 'facts', json('["bell-rang-1847","sunset-bell-not-fixed-hour"]'), 'kind', 'solid'), json_object('id', 'marta-returns', 'at', '18:49', 'place', '薬剤庫', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-returned-after-bell"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '19:10', 'place', '記録室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ニコロ・フェッリ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ピエトロ・サルヴィ' LIMIT 1)), 'facts', json('["body-found-1910"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"marta-before-bell-alibi","about":"marta-returned-after-bell"},{"id":"marta-no-shortage","about":"marta-diverted-medicine"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"nicolo-clean-ledger","about":"nicolo-altered-ration-ledger"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ニコロ・フェッリ'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"pietro-no-private-letter","about":"pietro-smuggled-letter"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ピエトロ・サルヴィ'; +--> statement-breakpoint +UPDATE evidences SET description = '事件当日は日没の鐘を18時47分に鳴らした記録があり、別の日には異なる時刻が並んでいる。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND label = '鐘の当直帳'; +--> statement-breakpoint +UPDATE evidences SET description = 'ニコロの業務メモには「鐘前、記録室側でマルタ」と書かれ、当直帳との照合で18時41分ごろと分かる。', contradicts = json('["lie:marta-before-bell-alibi"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND label = 'ニコロの廊下メモ'; +--> statement-breakpoint +UPDATE evidences SET description = '高価な薬剤の実数と帳簿が合わず、ロレンツォが翌朝の報告を示す書き込みを残している。', contradicts = json('["lie:marta-no-shortage"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND label = '不足した薬剤の帳簿'; +--> statement-breakpoint +UPDATE evidences SET description = 'ピエトロの規則違反を示す私信が見つかるが、記録室の事件とは結びつかない。', contradicts = json('["lie:pietro-no-private-letter"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND label = '舟に隠された私信'; +--> statement-breakpoint +UPDATE scenarios SET title = '霧都地下工事録', victim_found_in = '測量室', victim_estimated_death_at = '22:03' WHERE victim_name = 'エドワード・ヘイル'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'hale-confronts-bell', 'at', '21:35', 'place', '事務室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-bid-fraud","helale-confronted-bell"]'), 'kind', 'claim'), json_object('id', 'bell-enters-tunnel', 'at', '21:52', 'place', '連絡坑道', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-left-shaft-one"]'), 'kind', 'solid'), json_object('id', 'thomas-sees-bell', 'at', '21:58', 'place', '坑道', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'トーマス・リード' LIMIT 1)), 'facts', json('["thomas-saw-bell-tunnel"]'), 'kind', 'solid'), json_object('id', 'hale-death', 'at', '22:03', 'place', '測量室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-killed-hale"]'), 'kind', 'claim'), json_object('id', 'false-telegram', 'at', '22:12', 'place', '第2立坑', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-sent-telegram"]'), 'kind', 'claim'), json_object('id', 'telegram-received', 'at', '22:12', 'place', '第1立坑', 'room', '', 'record', '受信記録', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'クララ・ウェッブ' LIMIT 1)), 'facts', json('["telegram-received-2212"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:30', 'place', '測量室', 'room', '', 'record', '', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'クララ・ウェッブ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'トーマス・リード' LIMIT 1)), 'facts', json('["body-found-2230"]'), 'kind', 'solid')) WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1); +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"bell-stayed-shaft-one","about":"bell-left-shaft-one"},{"id":"bell-mark-unknown","about":"victim-private-mark-known-bell"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"clara-no-copying","about":"clara-copied-private-wire"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'クララ・ウェッブ'; +--> statement-breakpoint +UPDATE characters SET lie_refs = json('[{"id":"thomas-no-coal-theft","about":"thomas-sold-coal"}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'トーマス・リード'; +--> statement-breakpoint +UPDATE evidences SET description = '第一立坑の受信簿には電文の内容と受信時刻だけがあり、第二立坑で誰がキーを叩いたかは記録されていない。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND label = '22時12分の受信簿'; +--> statement-breakpoint +UPDATE evidences SET description = 'トーマスは21時58分ごろ、第二立坑寄りの坑道でベルを見ている。', contradicts = json('["lie:bell-stayed-shaft-one"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND label = '第二立坑側での目撃'; +--> statement-breakpoint +UPDATE evidences SET description = '過去の工程電文にも同じ末尾符号が何度も現れ、施工監督のベルが日常的に閲覧していた。', contradicts = json('["lie:bell-mark-unknown"]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND label = '過去の工程電文'; +--> statement-breakpoint +UPDATE evidences SET description = 'ベルの管理分だけ資材費が不自然に増えており、ヘイルが翌朝会社へ提出する印を付けている。', contradicts = json('[]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND label = '水増しされた資材帳簿'; diff --git a/db/migrations/0015_scenario-title-rethink.sql b/db/migrations/0015_scenario-title-rethink.sql new file mode 100644 index 0000000..7066671 --- /dev/null +++ b/db/migrations/0015_scenario-title-rethink.sql @@ -0,0 +1,85 @@ +UPDATE scenarios SET title = '白樺峰、明朝まで' WHERE victim_name = '早瀬隆司'; +--> statement-breakpoint +UPDATE scenarios SET title = '祈りの山は雪に閉ざされる' WHERE victim_name = '高瀬静一'; +--> statement-breakpoint +UPDATE scenarios SET title = '研修はもう終わった' WHERE victim_name = '塚本誠'; +--> statement-breakpoint +UPDATE scenarios SET title = '救助隊が来るまで地下にいる' WHERE victim_name = '岩代圭吾'; +--> statement-breakpoint +UPDATE scenarios SET title = 'あの冬、白樺館にいた' WHERE victim_name = '野上修一'; +--> statement-breakpoint +UPDATE scenarios SET title = 'アビス3、浮上不能' WHERE victim_name = '篠宮亮'; +--> statement-breakpoint +UPDATE scenarios SET title = 'しおかぜは霧を抜けない' WHERE victim_name = '柴田功'; +--> statement-breakpoint +UPDATE scenarios SET title = '八坂野の灯が消えるころ' WHERE victim_name = '神谷宗一'; +--> statement-breakpoint +UPDATE scenarios SET title = '水は紙より先に来る' WHERE victim_name = '今泉孝臣'; +--> statement-breakpoint +UPDATE scenarios SET title = '旧南央裁判所には、もう裁判がない' WHERE victim_name = '磯崎章'; +--> statement-breakpoint +UPDATE scenarios SET title = '内覧会のあと、絵は黙る' WHERE victim_name = '鳥羽薫'; +--> statement-breakpoint +UPDATE scenarios SET title = '星のない船で暮らす' WHERE victim_name = 'ミラ・ヴォス'; +--> statement-breakpoint +UPDATE scenarios SET title = '花が起きる前に' WHERE victim_name = '木島祥子'; +--> statement-breakpoint +UPDATE scenarios SET title = '原稿は人を殺さない' WHERE victim_name = '石橋礼司'; +--> statement-breakpoint +UPDATE scenarios SET title = '時計博物館に朝は遠い' WHERE victim_name = '倉橋宗一'; +--> statement-breakpoint +UPDATE scenarios SET title = '崖の上にホテルがひとつ' WHERE victim_name = '長峰宗一'; +--> statement-breakpoint +UPDATE scenarios SET title = '終電のあとに駅は残る' WHERE victim_name = '藤崎正雄'; +--> statement-breakpoint +UPDATE scenarios SET title = '火星基地に雨は降らない' WHERE victim_name = 'エレナ・ヴァルガ'; +--> statement-breakpoint +UPDATE scenarios SET title = 'ラジオ局には朝がない' WHERE victim_name = '大門修一'; +--> statement-breakpoint +UPDATE scenarios SET title = '第六基地に夜は来ない' WHERE victim_name = '牧瀬航'; +--> statement-breakpoint +UPDATE scenarios SET title = '白嶺診療所は本日休診' WHERE victim_name = '星名悟'; +--> statement-breakpoint +UPDATE scenarios SET title = '雨の日には古書を買わない' WHERE victim_name = '水野英治'; +--> statement-breakpoint +UPDATE scenarios SET title = '上海、雨は倉庫街に降る' WHERE victim_name = '周文海'; +--> statement-breakpoint +UPDATE scenarios SET title = '彫刻は雪を見ている' WHERE victim_name = '青沼卓'; +--> statement-breakpoint +UPDATE scenarios SET title = '星の見えない観測所' WHERE victim_name = '神崎遼'; +--> statement-breakpoint +UPDATE scenarios SET title = '高原農園は朝を待つ' WHERE victim_name = '佐久間隆志'; +--> statement-breakpoint +UPDATE scenarios SET title = '白環館には冬しかない' WHERE victim_name = '荻原直哉'; +--> statement-breakpoint +UPDATE scenarios SET title = 'ノース・レイクに冬が来た' WHERE victim_name = '冬木圭介'; +--> statement-breakpoint +UPDATE scenarios SET title = '幕が下りても帰れない' WHERE victim_name = '瀬尾雅人'; +--> statement-breakpoint +UPDATE scenarios SET title = '夜の水槽に客はいない' WHERE victim_name = '江波慎吾'; +--> statement-breakpoint +UPDATE scenarios SET title = '発電所は雨を止められない' WHERE victim_name = '峰岸達也'; +--> statement-breakpoint +UPDATE scenarios SET title = '船の来ない島で' WHERE victim_name = '榊原宗一'; +--> statement-breakpoint +UPDATE scenarios SET title = '夕凪という名の嵐' WHERE victim_name = '倉橋徹'; +--> statement-breakpoint +UPDATE scenarios SET title = '雨の梢庵でお待ちください' WHERE victim_name = '桐谷宗介'; +--> statement-breakpoint +UPDATE scenarios SET title = '星見ヶ丘は今夜も曇り' WHERE victim_name = '犬塚誠'; +--> statement-breakpoint +UPDATE scenarios SET title = 'みなもは港へ帰れない' WHERE victim_name = '瀬尾俊'; +--> statement-breakpoint +UPDATE scenarios SET title = '研究会はまだ終わらない' WHERE victim_name = '鷺沢修'; +--> statement-breakpoint +UPDATE scenarios SET title = '山頂駅には下りがない' WHERE victim_name = '高瀬修司'; +--> statement-breakpoint +UPDATE scenarios SET title = '十七回忌の客' WHERE victim_name = '高瀬涼子'; +--> statement-breakpoint +UPDATE scenarios SET title = '台風の日もサーバは眠らない' WHERE victim_name = '真田啓介'; +--> statement-breakpoint +UPDATE scenarios SET title = '台風圏、海上勤務' WHERE victim_name = '芳賀俊介'; +--> statement-breakpoint +UPDATE scenarios SET title = '検疫島で日が暮れる' WHERE victim_name = 'ロレンツォ・ヴァーレ'; +--> statement-breakpoint +UPDATE scenarios SET title = '地下鉄はまだ完成していない' WHERE victim_name = 'エドワード・ヘイル'; diff --git a/db/migrations/0016_scenario-title-balance.sql b/db/migrations/0016_scenario-title-balance.sql new file mode 100644 index 0000000..3977012 --- /dev/null +++ b/db/migrations/0016_scenario-title-balance.sql @@ -0,0 +1 @@ +-- Custom SQL migration file, put your code below! -- \ No newline at end of file diff --git a/db/migrations/0017_scenario-title-balance-data.sql b/db/migrations/0017_scenario-title-balance-data.sql new file mode 100644 index 0000000..caccde9 --- /dev/null +++ b/db/migrations/0017_scenario-title-balance-data.sql @@ -0,0 +1,16 @@ +UPDATE scenarios SET title = '霧航船しおかぜ号の謎' WHERE title = 'しおかぜは霧を抜けない'; +UPDATE scenarios SET title = '旧南央裁判所殺人事件' WHERE title = '旧南央裁判所には、もう裁判がない'; +UPDATE scenarios SET title = '世代船アステリア事件' WHERE title = '星のない船で暮らす'; +UPDATE scenarios SET title = '零時放送レイライン' WHERE title = 'ラジオ局には朝がない'; +UPDATE scenarios SET title = '白夜第六基地の三人' WHERE title = '第六基地に夜は来ない'; +UPDATE scenarios SET title = '青雨堂、雨宿りの客' WHERE title = '雨の日には古書を買わない'; +UPDATE scenarios SET title = '北岳観測所殺人事件' WHERE title = '星の見えない観測所'; +UPDATE scenarios SET title = '白環館の雪' WHERE title = '白環館には冬しかない'; +UPDATE scenarios SET title = '白燕座、幕間' WHERE title = '幕が下りても帰れない'; +UPDATE scenarios SET title = '深夜水族館の謎' WHERE title = '夜の水槽に客はいない'; +UPDATE scenarios SET title = '増水発電所' WHERE title = '発電所は雨を止められない'; +UPDATE scenarios SET title = '青凪荘の客' WHERE title = '船の来ない島で'; +UPDATE scenarios SET title = '調査船みなも号事件' WHERE title = 'みなもは港へ帰れない'; +UPDATE scenarios SET title = '山中研究会殺人録' WHERE title = '研究会はまだ終わらない'; +UPDATE scenarios SET title = '霧岳ロープウェイ殺人事件' WHERE title = '山頂駅には下りがない'; +UPDATE scenarios SET title = '湾岸データセンター、異常なし' WHERE title = '台風の日もサーバは眠らない'; diff --git a/db/migrations/0018_scenario-required-records.sql b/db/migrations/0018_scenario-required-records.sql new file mode 100644 index 0000000..ee39f46 --- /dev/null +++ b/db/migrations/0018_scenario-required-records.sql @@ -0,0 +1,334 @@ +-- Backfill timeline record labels required by the current authoring guide. +-- Only timeline_events[].record is changed; scenario/character UUIDs remain untouched. + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '伝言紙') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '工事タグ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[2].record', '席別得点表') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '青席の得点') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '軌跡比較') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '位置履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '位置履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '点検記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '署名記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '乗降記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '点呼表') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '制御盤ログ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '復旧ログ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '扉記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '扉記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '予備開錠記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '扉状態記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '扉状態記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '装置電源履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '装置電源履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[7].record', '装置電源履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '区画昼夜表') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '区画昼夜表') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '水道メーター') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[2].record', 'ミスト作動記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '印刷履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '同期試験票') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '防災端末記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '床の移動跡') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '衣紋掛けの上着') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '運行記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '印刷履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '通信記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[7].record', '自動送出ログ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '同期ずれ記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '入室履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[2].record', '整備時刻メモ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[2].record', '認証記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[8].record', '受付') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '搬出伝票') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '積雪層') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '除雪作業記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '無傷の雪面') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[2].record', '撮影設定') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '朝飼い作業板') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '台紙片') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[2].record', '台車の使用跡') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '作業着の台紙粉') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '環境管理記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', 'ループ素材') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '収録ファイル') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '調光卓履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', 'キュー履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', 'キュー履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '給餌設定') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '給餌作動記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '水位記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '作業靴の泥') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '水位記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '当直日誌') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '作成記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '予定送信設定') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '送信記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '封鎖確認票') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[2].record', '計測記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '封鎖確認票') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[4].record', '物入れの杖') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '点検表') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '消失確認メモ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '甲板メモ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '閲覧卓の位置') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '工具ケース') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '乗員表示') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '乗員表示') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[8].record', '飲みかけのグラス') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', 'ジョブ登録履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '搬入用ラッチ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '実行ログ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[0].record', '飛行モード履歴') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[1].record', '飛行ログ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[6].record', '飛行ログ') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[3].record', '当直帳') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ'); + +UPDATE scenario_truths +SET timeline_events = json_set(timeline_events, '$[5].record', '受信記録') +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル'); diff --git a/db/migrations/0019_investigable-places.sql b/db/migrations/0019_investigable-places.sql new file mode 100644 index 0000000..92e7a7e --- /dev/null +++ b/db/migrations/0019_investigable-places.sql @@ -0,0 +1,2 @@ +ALTER TABLE `scenario_truths` ADD `place_findings` text DEFAULT '[]' NOT NULL;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `places` text DEFAULT '[]' NOT NULL; \ No newline at end of file diff --git a/db/migrations/0020_deadline-disclosure.sql b/db/migrations/0020_deadline-disclosure.sql new file mode 100644 index 0000000..8140ecb --- /dev/null +++ b/db/migrations/0020_deadline-disclosure.sql @@ -0,0 +1 @@ +ALTER TABLE `evidences` ADD `reveals_death_time` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/db/migrations/0021_scenario-investigation-upgrade.sql b/db/migrations/0021_scenario-investigation-upgrade.sql new file mode 100644 index 0000000..07ece88 --- /dev/null +++ b/db/migrations/0021_scenario-investigation-upgrade.sql @@ -0,0 +1,381 @@ +-- Upgrade the reviewed scenarios for investigable places and disclosed death estimates. + +-- Existing scenario, character, and evidence UUIDs are preserved whenever rows already exist. + +UPDATE scenarios SET places = '[{"id":"choir-access","name":"聖歌席裏の点検口","shortName":"点検口","introduction":"鐘の修復作業で使われた、聖歌席裏の設備点検口","situation":"修復用の資材札が残る、小さな点検区画"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"choir-access","findings":[{"id":"test-line-still-present","statement":"点検口の奥に仮設試験線が残り、工事タグも撤去済みの状態にはなっていない。","requires":{"revelations":[],"evidences":[]}},{"id":"test-line-reaches-bell","statement":"仮設試験線は、鐘塔へ入らず一階側から鐘の作動確認を行える配線になっている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一'); + +UPDATE scenarios SET places = '[{"id":"survey-zone","name":"自動測量区画","shortName":"測量区画","introduction":"測量カートの校正と位置タグ確認を行う研究区画","situation":"カートと位置タグの充電台が壁沿いに並んでいる"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"survey-zone","findings":[{"id":"cart-tag-fastener","statement":"測量カートの収納ベルトには、小型の位置タグを固定できる留め具と新しい擦れ跡がある。","requires":{"revelations":[],"evidences":[]}},{"id":"cart-track-overlay","statement":"端末に残るカートの走行軌跡と位置タグの軌跡は、同じ時間帯に同じ経路を通っている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾'); + +UPDATE scenarios SET places = '[{"id":"case-archive","name":"旧捜査資料箱","shortName":"旧捜査資料","introduction":"1979年事件の調書・検視記録・現場写真をまとめた保管箱","situation":"黄ばんだ封筒と写真袋が、作成日順に綴じ直されている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '野上修一'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"case-archive","findings":[{"id":"original-autopsy-time","statement":"当時の検視記録には、死亡は22時05分ごろと見積もられた旨が記されている。","requires":{"revelations":[],"evidences":[]}},{"id":"original-sighting-source","statement":"事件直後の供述調書で22時30分の暖炉前目撃を自分の体験として述べているのは、一人だけである。","requires":{"revelations":[],"evidences":[]}},{"id":"old-expense-ledger","statement":"押収資料の仕入れ帳には、倉田の担当欄の金額を野上が事件当日に再確認した印が残っている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一'); + +UPDATE scenarios SET places = '[{"id":"rare-vault","name":"希少資料庫","shortName":"資料庫","introduction":"貴重資料を保管する、自動施錠式の資料庫","situation":"重い扉が閉じ、廊下側には開錠用の鍵穴がある"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"rare-vault","findings":[{"id":"self-locking-latch","statement":"扉は廊下側から開けるときだけ鍵を使い、外へ出て閉じればラッチが自動で掛かる構造になっている。","requires":{"revelations":[],"evidences":[]}},{"id":"door-contact-window","statement":"扉センサーには21時09分から21時22分まで開放が続き、その後に閉じた記録が残っている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣'); + +UPDATE scenarios SET places = '[{"id":"restoration-room","name":"修復室","shortName":"修復室","introduction":"紫外線検査装置と修復用の作業台がある部屋","situation":"検査装置は停止し、作業台だけが照明に浮かんでいる"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"restoration-room","findings":[{"id":"uv-power-gap","statement":"紫外線検査装置の履歴には、18時37分から18時51分まで電源が切れていた空白がある。","requires":{"revelations":[],"evidences":[]}},{"id":"uv-resume-time","statement":"装置は18時51分に再起動しており、その前の検査状態が連続していたわけではない。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫'); + +UPDATE scenarios SET places = '[{"id":"master-clock","name":"親時計盤","shortName":"親時計","introduction":"館内の展示時計と時報へ基準時刻を配る同期盤","situation":"保守扉に夕方の同期試験票が挟まれたままになっている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"master-clock","findings":[{"id":"master-clock-offset","statement":"親時計盤の補正値は正しい時刻より十一分進む設定になっている。","requires":{"revelations":[],"evidences":[]}},{"id":"independent-security-clock","statement":"防災端末の時刻は親時計盤と別系統で、同じ瞬間を十一分早い数字で記録している。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一'); + +UPDATE scenarios SET places = '[{"id":"playout-room","name":"自動送出室","shortName":"送出室","introduction":"収録音源と深夜番組の放送順を管理する送出卓","situation":"深夜番組の送出キューが画面に残っている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '大門修一'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"playout-room","findings":[{"id":"midnight-queued-audio","statement":"午前零時の番組冒頭には、事前収録された音声ファイルが自動送出対象として登録されている。","requires":{"revelations":[],"evidences":[]}},{"id":"playout-execution-log","statement":"実行ログでは、午前零時の音声は人のマイク操作ではなく送出卓から自動再生されている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一'); + +UPDATE scenarios SET places = '[{"id":"badge-reader","name":"検査廊下の認証端末","shortName":"認証端末","introduction":"防護区画を通る管理バッジの認証端末","situation":"通過履歴が時刻とバッジ番号の順で表示されている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '星名悟'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"badge-reader","findings":[{"id":"reader-records-badge","statement":"端末が保存しているのは通過したバッジ番号で、装着者の顔や氏名を記録する機能はない。","requires":{"revelations":[],"evidences":[]}},{"id":"orange-badge-entry","statement":"21時18分には橙色の管理バッジが検査廊下を通過した記録が残っている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟'); + +UPDATE scenarios SET places = '[{"id":"framing-room","name":"額装作業室","shortName":"額装室","introduction":"作品台紙の加工と搬送準備を行う作業室","situation":"清掃後の床に、細かな紙粉がまだ残っている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"framing-room","findings":[{"id":"framing-paper-traces","statement":"床に散った紙片は、その夜に荻原が確認していた作品台紙と同じ材質である。","requires":{"revelations":[],"evidences":[]}},{"id":"cart-used-again","statement":"清掃後に戻された作品搬送台車には、その後もう一度動かされた車輪跡が残っている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉'); + +UPDATE scenarios SET places = '[{"id":"patch-bay","name":"第2ブース監視席","shortName":"監視席","introduction":"録音入力を切り替えるパッチ盤と収録端末の席","situation":"直前の収録セッションの配線がそのまま残っている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"patch-bay","findings":[{"id":"recorder-loop-route","statement":"第2ブースの録音入力は生マイクではなく、四十七秒の音声素材を繰り返す経路へ切り替えられている。","requires":{"revelations":[],"evidences":[]}},{"id":"waveform-identical","statement":"収録波形は空調音や小さな物音まで四十七秒ごとに同じ形を繰り返している。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介'); + +UPDATE scenarios SET places = '[{"id":"old-walkway","name":"旧保守歩廊","shortName":"旧歩廊","introduction":"排水区画を通り、旧制御室の接続口へ続く古い通路","situation":"現在は増水で水に覆われ、入口から先へ進めない"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"old-walkway","findings":[{"id":"walkway-connects-control","statement":"設備図と入口の表示から、この歩廊は正面扉を通らず旧制御室へ入れる接続口まで続いている。","requires":{"revelations":[],"evidences":[]}},{"id":"reddish-silt-floor","statement":"水際より手前の床には、この歩廊周辺に特有の赤褐色の堆積泥が残っている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也'); + +UPDATE scenarios SET places = '[{"id":"old-stairs","name":"厨房脇の旧階段","shortName":"旧階段","introduction":"二階廊下へ木の隔壁一枚で接する、普段使われない階段","situation":"古い木造の隔壁と手すりがそのまま残されている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"old-stairs","findings":[{"id":"partition-carries-taps","statement":"手すりを軽く叩くと、木の中空隔壁を通って二階廊下側へ乾いた音がよく響く。","requires":{"revelations":[],"evidences":[]}},{"id":"cane-height-marks","statement":"隔壁には、桐谷の杖の金属部分と高さの合う新しい打痕が残っている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介'); + +UPDATE scenarios SET places = '[{"id":"observation-glass","name":"投影室の観察ガラス","shortName":"観察ガラス","introduction":"投影室と廊下を隔てる、大型の観察窓","situation":"閉館後の点検で投影室側の主照明が落とされている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"observation-glass","findings":[{"id":"corridor-reflection","statement":"投影室を暗くして廊下側を明るくすると、ガラスには廊下側に立つ人物の像が強く映り込む。","requires":{"revelations":[],"evidences":[]}},{"id":"witness-position-replay","statement":"21時35分の立ち位置を再現すると、廊下にいた人物の像が投影室内の人影のように重なる。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠'); + +UPDATE scenarios SET places = '[{"id":"emergency-cabin","name":"非常用搬器","shortName":"非常搬器","introduction":"点検席と乗員表示センサーを備えた非常用の搬器","situation":"営業終了後の点検位置で停止している"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"emergency-cabin","findings":[{"id":"seat-senses-weight","statement":"点検席のセンサーは人物を識別せず、一定以上の重量が掛かると「乗員1」を表示する。","requires":{"revelations":[],"evidences":[]}},{"id":"tool-case-triggers-seat","statement":"保守用工具ケースだけを点検席へ置いても、監視盤の表示は「乗員1」へ切り替わる。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司'); + +UPDATE scenarios SET places = '[{"id":"shaft-two-telegraph","name":"第二立坑の電信機","shortName":"第2電信","introduction":"二つの立坑を結ぶ、工事連絡用の電信機","situation":"送信キーと符号表が作業机の上に残されている"}]' WHERE id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル'); + +UPDATE scenario_truths SET place_findings = '[{"placeId":"shaft-two-telegraph","findings":[{"id":"sender-not-recorded","statement":"電信機と受信簿が残すのは電文と受信時刻だけで、第二立坑で誰が送信キーを操作したかは記録されない。","requires":{"revelations":[],"evidences":[]}},{"id":"mark-can-be-copied","statement":"過去の工程電文にはヘイルが使う短い末尾符号が何度も残り、作業関係者が見られる状態になっている。","requires":{"revelations":[],"evidences":[]}}]}]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル'); + +UPDATE scenarios SET victim_estimated_death_at = '21:05' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一'); + +UPDATE scenarios SET victim_estimated_death_at = '22:05' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '野上修一'); + +UPDATE scenarios SET victim_estimated_death_at = '05:57' WHERE id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス'); + +UPDATE scenarios SET victim_estimated_death_at = '20:41' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司'); + +UPDATE scenarios SET victim_estimated_death_at = '20:28' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一'); + +UPDATE scenarios SET victim_estimated_death_at = '21:05' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一'); + +UPDATE scenarios SET victim_estimated_death_at = '23:48' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '大門修一'); + +UPDATE scenarios SET victim_estimated_death_at = '21:08' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '星名悟'); + +UPDATE scenarios SET victim_estimated_death_at = '21:52' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '周文海'); + +UPDATE scenarios SET victim_estimated_death_at = '2026-01-15T22:10:00+09:00' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志'); + +UPDATE scenarios SET victim_estimated_death_at = '21:28' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一'); + +UPDATE scenarios SET victim_estimated_death_at = '21:08' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介'); + +UPDATE scenarios SET victim_estimated_death_at = '21:24' WHERE id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠'); + +UPDATE scenarios SET victim_estimated_death_at = '22:03' WHERE id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル'); + +UPDATE scenarios SET victim_investigable = 0 WHERE id = (SELECT id FROM scenarios WHERE victim_name = '野上修一'); + +UPDATE scenario_truths SET victim_cause_of_death = NULL, victim_findings = '[]' WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一'); + +UPDATE evidences +SET description = '点検口には仮設試験線がまだ接続中であることを示す工事タグが残っている。', + reveal_condition = '玄田に聖歌席裏の点検口で見たものを尋ねるか、水城に仮設線の撤去状況を確認したら開示する。または聖歌席裏の点検口を調べ、残った工事タグと仮設線を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一') AND name = '玄田修')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一') AND name = '水城奈央')), json_object('type', 'location', 'id', 'choir-access')), + supports = '["test-line-tag-remained","temporary-test-line-exists"]', + contradicts = '["lie:mizuki-bell-alibi"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一') + AND label = '聖歌席裏の工事タグ'; + +UPDATE evidences +SET description = '香川の位置タグと自動測量カートが、21時00分から15分まで同じ地点を同じ時刻に通過している。二つの軌跡は実質的に重なる。', + reveal_condition = '香川、結城、新堂のいずれかに位置タグの軌跡と自動測量カートの運行を比較できないか尋ねたら開示する。または自動測量区画を調べ、カートの走行跡と位置タグの固定跡を照合したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾') AND name = '香川紗英')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾') AND name = '結城真')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾') AND name = '新堂匠')), json_object('type', 'location', 'id', 'survey-zone')), + supports = '["locator-tag-removable","mapping-cart-auto-loop","kagawa-tag-on-cart","tag-track-matches-cart"]', + contradicts = '["lie:kagawa-location-alibi","lie:kagawa-wore-tag"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾') + AND label = '位置タグと自動測量カートの軌跡比較'; + +UPDATE evidences +SET description = '倉田の担当欄で仕入れ額が実際より増やされ、野上が事件当日に再確認の印を付けている。', + reveal_condition = '倉田に野上と事件直前に揉めた帳簿の内容を追及したら開示する。または旧捜査資料箱を調べ、押収された仕入れ帳の該当欄を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一') AND name = '倉田恵')), json_object('type', 'location', 'id', 'case-archive')), + supports = '["megumi-forged-expenses","nogami-found-megumi-fraud"]', + contradicts = '["lie:megumi-no-fraud"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一') + AND label = '仕入れ帳の水増し'; + +UPDATE evidences +SET description = '廊下側から開ける時だけ館長鍵が必要で、外へ出て扉を閉めるとラッチが自動で掛かる。施錠操作に鍵は不要である。', + reveal_condition = '八神か戸塚に資料庫の鍵が開錠と施錠のどちらに必要なのか具体的に尋ねたら開示する。または希少資料庫の扉を調べ、閉じるだけで施錠されるラッチ構造を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣') AND name = '八神琴子')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣') AND name = '戸塚誠')), json_object('type', 'location', 'id', 'rare-vault')), + supports = '["vault-key-only-opens","vault-self-locks","key-found-on-imaizumi"]', + contradicts = '["lie:yagami-key-needed-to-lock"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣') + AND label = '希少資料庫の自動施錠仕様'; + +UPDATE evidences +SET description = '装置は18時35分に起動したが、18時37分から18時51分まで停止している。', + reveal_condition = '榊に検査を続けていた時間帯を尋ねるか、榎本に修復室の機器ログを確認できないか尋ねたら開示する。または修復室を調べ、紫外線検査装置の電源履歴を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫') AND name = '榊玲')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫') AND name = '榎本駿')), json_object('type', 'location', 'id', 'restoration-room')), + supports = '["uv-test-started","uv-lamp-off-1837","uv-test-resumed"]', + contradicts = '["lie:sakaki-uv-alibi"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫') + AND label = '紫外線検査装置の電源履歴'; + +UPDATE evidences +SET description = '防災端末が20時19分を記録した瞬間の監視画像で、中央ホールの親時計は20時30分を示している。両系統には十一分の差がある。', + reveal_condition = '城戸か保科に館内時計の精度と、防災端末など別系統の時計との比較を尋ねたら開示する。または親時計盤を調べ、防災端末と表示時刻を突き合わせたら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一') AND name = '城戸篤')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一') AND name = '保科悠人')), json_object('type', 'location', 'id', 'master-clock')), + supports = '["master-clock-fast-eleven","gallery-clocks-follow-master","security-clock-accurate","half-past-chime-actual-2019"]', + contradicts = '["lie:kido-clock-accurate","lie:shiba-late-last-seen"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一') + AND label = '親時計と防災端末の時刻差'; + +UPDATE evidences +SET description = '午前零時の冒頭素材は23時53分に登録され、時刻指定で自動再生された記録が残る。', + reveal_condition = '久世に午前零時の音声が生放送か録音かを尋ねるか、美濃部の「生だった」という説明の技術的根拠を確認したら開示する。または自動送出室を調べ、午前零時の送出キューと実行ログを確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一') AND name = '久世直人')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一') AND name = '美濃部沙耶')), json_object('type', 'location', 'id', 'playout-room')), + supports = '["minobe-queued-recording","recorded-opening-aired"]', + contradicts = '["lie:minobe-live-alibi"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一') + AND label = '自動送出システムの実行ログ'; + +UPDATE evidences +SET description = '記録されているのは橙色バッジの通過であり、使用者の顔や氏名を直接確認した記録ではない。', + reveal_condition = '相原か久我に21時18分の認証記録が何を識別しているのか尋ねたら開示する。または検査廊下の認証端末を調べ、記録がバッジ番号だけで使用者を識別しないと確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟') AND name = '相原誠')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟') AND name = '久我夏樹')), json_object('type', 'location', 'id', 'badge-reader')), + supports = '["orange-badge-passed-2118","aihara-saw-orange-suit"]', + contradicts = '["lie:sera-badge-proves-hoshina"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟') + AND label = '二十一時十八分の認証記録'; + +UPDATE evidences +SET description = '清掃後の額装作業室に、荻原が確認していた作品台紙と同じ紙片が散っている。', + reveal_condition = '朝倉に事件後の額装作業室で気づいた変化を尋ねたら開示する。または額装作業室を調べ、床の台紙片と作業台周辺の痕跡を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉') AND name = '朝倉凪')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉') AND name = '香坂澪')), json_object('type', 'location', 'id', 'framing-room')), + supports = '["framing-paper-fibers","kosaka-killed-ogiwara-framing"]', + contradicts = '["lie:kosaka-vault-crime"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉') + AND label = '額装作業室の台紙片'; + +UPDATE evidences +SET description = '空調音や小さな物音まで含めた波形が四十七秒周期で完全一致し、同じ室内音が繰り返し再生されていたと分かる。', + reveal_condition = '真鍋か牧村に収録ファイルが本当に生マイク入力だったか、波形の反復と入力経路を含めて尋ねたら開示する。または第2ブースの監視席を調べ、録音入力と波形の反復を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介') AND name = '真鍋伊織')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介') AND name = '牧村葉月')), json_object('type', 'location', 'id', 'patch-bay')), + supports = '["manabe-made-roomtone-loop","loop-routed-to-recorder","recording-repeats-identically"]', + contradicts = '["lie:manabe-booth-alibi","lie:manabe-live-input"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介') + AND label = '四十七秒ごとに一致する収録波形'; + +UPDATE evidences +SET description = '正面扉とは別に、排水区画側から旧制御室へ接続する保守歩廊がある。', + reveal_condition = '城戸に旧制御室の保守経路を尋ねるか、谷口に設備図上の接続を確認したら開示する。または旧保守歩廊を調べ、旧制御室への接続口を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也') AND name = '城戸真琴')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也') AND name = '谷口航')), json_object('type', 'location', 'id', 'old-walkway')), + supports = '["old-walkway-connects"]', + contradicts = '["lie:kido-impossible-entry"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也') + AND label = '旧保守歩廊の接続図'; + +UPDATE evidences +SET description = '二つの区画は同じ古い中空隔壁に接し、旧階段側の硬い音が二階から聞こえることがある。', + reveal_condition = '榊原に古い建物の音の伝わり方を尋ねるか、秋庭に旧階段で音が響くことを知っていたか確認したら開示する。または厨房脇の旧階段を調べ、隔壁の構造と音の伝わり方を確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介') AND name = '榊原蓮')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介') AND name = '秋庭澄')), json_object('type', 'location', 'id', 'old-stairs')), + supports = '["old-partition-transmits"]', + contradicts = '["lie:akiwa-cane-proves-alive"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介') + AND label = '旧階段と二階廊下の中空壁'; + +UPDATE evidences +SET description = '投影室を暗くして廊下を明るくすると、観察ガラスには室内より廊下側の人物が強く反射する。', + reveal_condition = '松田か小野寺に投影室消灯時の観察ガラスの見え方を具体的に尋ねたら開示する。または投影室の観察ガラスを調べ、消灯時の反射を同じ立ち位置で再現したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠') AND name = '松田圭介')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠') AND name = '小野寺莉香')), json_object('type', 'location', 'id', 'observation-glass')), + supports = '["booth-dark-2130","glass-reflects-dark-booth","onodera-knew-reflection"]', + contradicts = '["lie:onodera-sighting-was-inuzuka"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠') + AND label = '観察ガラスの照明条件テスト'; + +UPDATE evidences +SET description = '『乗員1』は一定以上の荷重で点灯し、保守用工具ケースだけでも同じ表示になる。21時台にはそのケースが点検席へ置かれていた。', + reveal_condition = '須藤、長峰、折原のいずれかに『乗員1』表示の検知方式と工具ケースについて尋ねたら開示する。または非常用搬器を調べ、座席センサーが重量だけを検知することと工具ケースの重さを確認したら開示する。', + sources = json_array(json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司') AND name = '須藤拓海')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司') AND name = '長峰礼')), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司') AND name = '折原壮')), json_object('type', 'location', 'id', 'emergency-cabin')), + supports = '["cabin-display-weight-based","tool-case-heavy-enough","sudo-left-tool-case","occupancy-display-on"]', + contradicts = '["lie:sudo-cabin-alibi","lie:sudo-no-toolcase-seat"]', + reveals_death_time = 0 +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司') + AND label = '非常用搬器の座席重量仕様'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '8a39e27f-c6a7-4a4b-877b-dd9267901bbc', (SELECT id FROM scenarios WHERE victim_name = '高瀬静一'), '発見時の死亡推定', '資料整理室の室温と発見時の状態を照合すると、高瀬が死亡したのは21時05分ごろと見積もられる。21時20分の鐘より前である。', '遺体を調べて発見時の状態から死亡時刻を推定するか、玄田に発見時の状態と確認内容を具体的に尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一') AND name = '玄田修'))), '["mizuki-killed-takase"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一') AND label = '発見時の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '発見時の高瀬の状態と冷えた資料整理室の様子を確認しており、死亡は21時05分ごろと見積もられるという確認内容を覚えている。') = 0 THEN memories || char(10) || '- 発見時の高瀬の状態と冷えた資料整理室の様子を確認しており、死亡は21時05分ごろと見積もられるという確認内容を覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一') AND name = '玄田修'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '59a5f86d-68fd-40fc-90b1-ae436d946355', (SELECT id FROM scenarios WHERE victim_name = '野上修一'), '1979年の検視記録', '当時の検視記録は、発見時の状態から野上の死亡を22時05分ごろと見積もっている。22時30分の暖炉前目撃より前になる。', '旧捜査資料箱を調べて当時の検視記録を確認するか、藤村に再調査で読み直した検視記録の内容を尋ねたら開示する。', json_array(json_object('type', 'location', 'id', 'case-archive'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一') AND name = '藤村達也'))), '["megumi-killed-nogami"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一') AND label = '1979年の検視記録' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '再調査で開封された旧検視記録を読み、当時の死亡推定が22時05分ごろだったことを改めて知っている。') = 0 THEN memories || char(10) || '- 再調査で開封された旧検視記録を読み、当時の死亡推定が22時05分ごろだったことを改めて知っている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一') AND name = '藤村達也'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '039f8a95-322e-49c5-abca-1a09a40a30ca', (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス'), '医療区の死亡推定', '医療センサーによる発見時の確認では、ミラの死亡は船内標準時05時57分ごろと見積もられる。区画ごとの人工時刻とは別の基準である。', '遺体を調べて医療センサーの確認値を見るか、医療担当のユナに死亡推定を船内標準時で尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス') AND name = 'ユナ・パク'))), '["sera-killed-mira"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス') AND label = '医療区の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '医療担当として発見時の状態を確認し、死亡は船内標準時05時57分ごろと見積もっている。人工昼夜表示ではなく標準時で記録した。') = 0 THEN memories || char(10) || '- 医療担当として発見時の状態を確認し、死亡は船内標準時05時57分ごろと見積もっている。人工昼夜表示ではなく標準時で記録した。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス') AND name = 'ユナ・パク'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '299254d1-c287-448f-9749-279f2e51d9c3', (SELECT id FROM scenarios WHERE victim_name = '石橋礼司'), '編集長室の死亡推定', '編集長室の室温と発見時の状態を合わせると、石橋の死亡は20時41分ごろと見積もられる。20時50分の署名時刻より前である。', '遺体を調べて発見時の状態から死亡時刻を推定するか、藤本に発見時に確認した状態と時刻について尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司') AND name = '藤本圭'))), '["kawase-killed-ishibashi-2041"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司') AND label = '編集長室の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '発見時の石橋の状態と編集長室の室温を確認しており、死亡は20時41分ごろと見積もられるという確認内容を覚えている。') = 0 THEN memories || char(10) || '- 発見時の石橋の状態と編集長室の室温を確認しており、死亡は20時41分ごろと見積もられるという確認内容を覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司') AND name = '藤本圭'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '7b6b0858-5fe6-4871-91ed-681914492c94', (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一'), '修復室の死亡推定', '発見時の状態を、親時計とは別系統の防災端末時刻で整理すると、倉橋の死亡は20時28分ごろと見積もられる。', '遺体を調べて発見時の状態を正しい時刻系で確認するか、保科に発見時の確認内容を防災端末の時刻基準で尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一') AND name = '保科悠人'))), '["shiba-killed-kurahashi"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一') AND label = '修復室の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '発見時の確認内容を防災端末の正しい時刻で控えており、倉橋の死亡は20時28分ごろと見積もられると覚えている。') = 0 THEN memories || char(10) || '- 発見時の確認内容を防災端末の正しい時刻で控えており、倉橋の死亡は20時28分ごろと見積もられると覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一') AND name = '保科悠人'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '50e6108a-a8bb-42a6-87f9-6634dd053925', (SELECT id FROM scenarios WHERE victim_name = '長峰宗一'), '執務室の死亡推定', '執務室の室温と発見時の状態から、長峰の死亡は21時05分ごろと見積もられる。21時30分に見えた人影より前である。', '遺体を調べて発見時の状態を確認するか、星野に最初に執務室へ入ったときの状態と確認内容を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一') AND name = '星野結'))), '["ayase-killed-nagamine"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一') AND label = '執務室の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '最初に執務室へ入ったときの状態を覚えており、確認では長峰の死亡は21時05分ごろと見積もられていた。') = 0 THEN memories || char(10) || '- 最初に執務室へ入ったときの状態を覚えており、確認では長峰の死亡は21時05分ごろと見積もられていた。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一') AND name = '星野結'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '7ca45f6f-7b0a-4814-bd7e-23352188e911', (SELECT id FROM scenarios WHERE victim_name = '大門修一'), '第2ブースの死亡推定', '第2ブースの一定した室温と発見時の状態を合わせると、大門の死亡は23時48分ごろと見積もられる。午前零時の放送より前である。', '遺体を調べて発見時の状態を確認するか、久世にブースの室温記録と発見時の確認内容を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一') AND name = '久世直人'))), '["daimon-died-2348"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一') AND label = '第2ブースの死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '第2ブースの空調温度と発見時の状態を確認しており、死亡は23時48分ごろと見積もられるという確認内容を覚えている。') = 0 THEN memories || char(10) || '- 第2ブースの空調温度と発見時の状態を確認しており、死亡は23時48分ごろと見積もられるという確認内容を覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一') AND name = '久世直人'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '89d36740-b882-4b74-91b9-08db60357d01', (SELECT id FROM scenarios WHERE victim_name = '星名悟'), '診療所の死亡推定', '診療所の検査機器で発見時の状態を確認すると、星名の死亡は21時08分ごろと見積もられる。21時18分のバッジ通過より前である。', '遺体を調べて検査機器の確認値を見るか、久我に発見時に取った検査値と死亡推定について尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟') AND name = '久我夏樹'))), '["hoshina-death-2108"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟') AND label = '診療所の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '検査担当として発見時の確認値を取り、星名の死亡は21時08分ごろと見積もられることを把握している。') = 0 THEN memories || char(10) || '- 検査担当として発見時の確認値を取り、星名の死亡は21時08分ごろと見積もられることを把握している。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟') AND name = '久我夏樹'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '78d82753-b74d-46a4-853f-e394d4a5ef4a', (SELECT id FROM scenarios WHERE victim_name = '周文海'), '当夜の検視メモ', '発見後に作られた検視メモでは、周の死亡は21時52分ごろと見積もられている。上紙へ22時05分が書き足されるより前である。', '遺体を調べて当夜の検視内容を確認するか、陳に発見後に控えた検視メモの時刻を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海') AND name = '陳伯安'))), '["lin-killed-zhou"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海') AND label = '当夜の検視メモ' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '発見後の検視内容を業務メモへ写しており、周の死亡は21時52分ごろと見積もられていたことを覚えている。') = 0 THEN memories || char(10) || '- 発見後の検視内容を業務メモへ写しており、周の死亡は21時52分ごろと見積もられていたことを覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海') AND name = '陳伯安'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT 'aced8865-9f30-45b4-ab44-b6383e8519b9', (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志'), '冬夜の死亡推定', '事務室の夜間温度と発見時の状態を合わせると、佐久間の死亡は前夜22時10分ごろと見積もられる。明け方の朝仕事より大幅に早い。', '遺体を調べて発見時の状態を確認するか、久世に事務室の夜間温度と発見時の確認内容を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志') AND name = '久世圭太'))), '["fuyuki-killed-sakuma"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志') AND label = '冬夜の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '設備担当として事務室の夜間温度を把握し、発見時の確認では佐久間の死亡は前夜22時10分ごろと見積もられていたことを覚えている。') = 0 THEN memories || char(10) || '- 設備担当として事務室の夜間温度を把握し、発見時の確認では佐久間の死亡は前夜22時10分ごろと見積もられていたことを覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志') AND name = '久世圭太'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '95442215-0b1b-4d9c-bc3b-47ecf3ae38a0', (SELECT id FROM scenarios WHERE victim_name = '榊原宗一'), '執務室の死亡推定', '執務室の室温と発見時の状態から、榊原の死亡は21時28分ごろと見積もられる。21時42分のメール送信より前である。', '遺体を調べて発見時の状態を確認するか、堀江に執務室へ入ったときの状態と確認内容を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一') AND name = '堀江充'))), '["aizawa-killed-sakakibara"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一') AND label = '執務室の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '発見時に執務室の状態を確認しており、榊原の死亡は21時28分ごろと見積もられるという確認内容を覚えている。') = 0 THEN memories || char(10) || '- 発見時に執務室の状態を確認しており、榊原の死亡は21時28分ごろと見積もられるという確認内容を覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一') AND name = '堀江充'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '547221fe-b63a-41a1-a9db-094323ceae92', (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介'), '帳場奥の死亡推定', '帳場奥の室温と発見時の状態から、桐谷の死亡は21時08分ごろと見積もられる。21時25分に聞かれた杖音より前である。', '遺体を調べて発見時の状態を確認するか、榊原に発見時の室内と確認内容を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介') AND name = '榊原蓮'))), '["akiwa-killed-kiritani"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介') AND label = '帳場奥の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '発見時の帳場奥の状態を確認しており、桐谷の死亡は21時08分ごろと見積もられるという確認内容を覚えている。') = 0 THEN memories || char(10) || '- 発見時の帳場奥の状態を確認しており、桐谷の死亡は21時08分ごろと見積もられるという確認内容を覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介') AND name = '榊原蓮'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '359e693f-6852-4e3b-9453-e9261328f789', (SELECT id FROM scenarios WHERE victim_name = '犬塚誠'), '投影準備室の死亡推定', '投影準備室の温度と発見時の状態を合わせると、犬塚の死亡は21時24分ごろと見積もられる。21時35分の人影目撃より前である。', '遺体を調べて発見時の状態を確認するか、松田に投影準備室へ入ったときの状態と確認内容を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠') AND name = '松田圭介'))), '["inuzuka-death-2124"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠') AND label = '投影準備室の死亡推定' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '発見時の投影準備室の状態を確認しており、犬塚の死亡は21時24分ごろと見積もられるという確認内容を覚えている。') = 0 THEN memories || char(10) || '- 発見時の投影準備室の状態を確認しており、犬塚の死亡は21時24分ごろと見積もられるという確認内容を覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠') AND name = '松田圭介'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT '7b4c8930-2002-410c-a66a-c0093f6a5876', (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル'), '現場の死亡推定記録', '測量室の室温と発見時の確認記録から、ヘイルの死亡は22時03分ごろと見積もられる。22時12分の電信より前である。', '遺体を調べて発見時の状態を確認するか、トーマスに測量室での発見時の状態と記録内容を尋ねたら開示する。', json_array(json_object('type', 'victim', 'id', 'victim'), json_object('type', 'character', 'id', (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル') AND name = 'トーマス・リード'))), '["bell-killed-hale"]', '[]', 1 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル') AND label = '現場の死亡推定記録' +); + +UPDATE characters +SET memories = CASE WHEN instr(memories, '測量室でヘイルを発見したときの状態を作業記録へ残しており、死亡は22時03分ごろと見積もられるという確認内容を覚えている。') = 0 THEN memories || char(10) || '- 測量室でヘイルを発見したときの状態を作業記録へ残しており、死亡は22時03分ごろと見積もられるという確認内容を覚えている。' ELSE memories END +WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル') AND name = 'トーマス・リード'; + +INSERT INTO evidences (id, scenario_id, label, description, reveal_condition, sources, supports, contradicts, reveals_death_time) +SELECT 'd1324f0e-bc53-48d0-9795-0df0539242df', (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル'), '第二立坑の電信機の送信仕様', '第二立坑の送信キーは操作者を識別せず、既知の符号列を誰でも同じ形で送れる。', '第二立坑の電信機を調べ、送信者を識別する仕組みがないことと符号表を確認したら開示する。', json_array(json_object('type', 'location', 'id', 'shaft-two-telegraph')), '["telegraph-no-sender-id"]', '[]', 0 +WHERE NOT EXISTS ( + SELECT 1 FROM evidences WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル') AND label = '第二立坑の電信機の送信仕様' +); diff --git a/db/migrations/0022_scenario-place-public-copy-safety.sql b/db/migrations/0022_scenario-place-public-copy-safety.sql new file mode 100644 index 0000000..57cd3c1 --- /dev/null +++ b/db/migrations/0022_scenario-place-public-copy-safety.sql @@ -0,0 +1,47 @@ +-- Keep investigable-place list copy public and spoiler-neutral. + +-- Private place findings are unchanged. + +UPDATE scenarios +SET places = '[{"id":"choir-access","name":"聖歌席裏","shortName":"聖歌席裏","introduction":"鐘の修復資材が置かれた、一階の作業区画","situation":"木箱と工事用の札がまとめて置かれている"}]' +WHERE victim_name = '高瀬静一'; + +UPDATE scenarios +SET places = '[{"id":"rare-vault","name":"希少資料庫","shortName":"資料庫","introduction":"貴重資料を保管する、厚い防火扉の資料庫","situation":"重い扉が閉じ、廊下側には開錠用の鍵穴がある"}]' +WHERE victim_name = '今泉孝臣'; + +UPDATE scenarios +SET places = '[{"id":"playout-room","name":"自動送出室","shortName":"送出室","introduction":"収録音源と深夜番組の放送順を管理する送出卓","situation":"送出卓のモニターと操作盤が待機状態になっている"}]' +WHERE victim_name = '大門修一'; + +UPDATE scenarios +SET places = '[{"id":"badge-reader","name":"検査廊下の認証端末","shortName":"認証端末","introduction":"防護区画を通る管理バッジの認証端末","situation":"認証端末の画面が待機表示のまま残っている"}]' +WHERE victim_name = '星名悟'; + +UPDATE scenarios +SET places = '[{"id":"framing-room","name":"額装作業室","shortName":"額装室","introduction":"作品台紙の加工と搬送準備を行う作業室","situation":"作業台と資材棚が、閉館時のまま残されている"}]' +WHERE victim_name = '荻原直哉'; + +UPDATE scenarios +SET places = '[{"id":"patch-bay","name":"第2ブース監視席","shortName":"監視席","introduction":"録音入力を切り替えるパッチ盤と収録端末の席","situation":"パッチ盤と収録端末の電源が残っている"}]' +WHERE victim_name = '冬木圭介'; + +UPDATE scenarios +SET places = '[{"id":"old-walkway","name":"旧保守歩廊","shortName":"旧歩廊","introduction":"排水区画に残る、現在は使われていない保守通路","situation":"現在は増水で水に覆われ、入口から先へ進めない"}]' +WHERE victim_name = '峰岸達也'; + +UPDATE scenarios +SET places = '[{"id":"old-stairs","name":"厨房脇の旧階段","shortName":"旧階段","introduction":"厨房脇に残る、普段使われない古い木造階段","situation":"古い木造の隔壁と手すりがそのまま残されている"}]' +WHERE victim_name = '桐谷宗介'; + +UPDATE scenarios +SET places = '[{"id":"observation-glass","name":"投影室の観察ガラス","shortName":"観察ガラス","introduction":"投影室と廊下を隔てる、大型の観察窓","situation":"投影室と廊下のあいだを、大きな一枚ガラスが隔てている"}]' +WHERE victim_name = '犬塚誠'; + +UPDATE scenarios +SET places = '[{"id":"emergency-cabin","name":"非常用搬器","shortName":"非常搬器","introduction":"非常時と保守点検に使う、小型の予備搬器","situation":"営業終了後の点検位置で停止している"}]' +WHERE victim_name = '高瀬修司'; + +UPDATE scenarios +SET places = '[{"id":"shaft-two-telegraph","name":"第二立坑の電信機","shortName":"第2電信","introduction":"二つの立坑を結ぶ、工事連絡用の電信機","situation":"送信キーと記録用の紙束が作業机の上に残されている"}]' +WHERE victim_name = 'エドワード・ヘイル'; diff --git a/db/migrations/meta/0009_snapshot.json b/db/migrations/meta/0009_snapshot.json new file mode 100644 index 0000000..fdd8250 --- /dev/null +++ b/db/migrations/meta/0009_snapshot.json @@ -0,0 +1,1099 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "07734c4f-2636-42dd-8648-e6efba562aac", + "prevId": "a685bdda-dd2c-4777-b964-be14a63bc341", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0010_snapshot.json b/db/migrations/meta/0010_snapshot.json new file mode 100644 index 0000000..72ca8ae --- /dev/null +++ b/db/migrations/meta/0010_snapshot.json @@ -0,0 +1,1136 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b2985582-8bb1-4034-8076-0d8e93048e81", + "prevId": "07734c4f-2636-42dd-8648-e6efba562aac", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0011_snapshot.json b/db/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..13db70e --- /dev/null +++ b/db/migrations/meta/0011_snapshot.json @@ -0,0 +1,1136 @@ +{ + "id": "f50f059d-d1f3-4c47-a327-6f99ff50158f", + "prevId": "b2985582-8bb1-4034-8076-0d8e93048e81", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0012_snapshot.json b/db/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..6a1fb18 --- /dev/null +++ b/db/migrations/meta/0012_snapshot.json @@ -0,0 +1,1150 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "bb814f94-27b7-47f5-8e1a-c9318447edbc", + "prevId": "f50f059d-d1f3-4c47-a327-6f99ff50158f", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0013_snapshot.json b/db/migrations/meta/0013_snapshot.json new file mode 100644 index 0000000..9fb4dbf --- /dev/null +++ b/db/migrations/meta/0013_snapshot.json @@ -0,0 +1,1166 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "91f9c18e-c5ec-4514-937b-85c8b49e6920", + "prevId": "bb814f94-27b7-47f5-8e1a-c9318447edbc", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0014_snapshot.json b/db/migrations/meta/0014_snapshot.json new file mode 100644 index 0000000..c16fe5e --- /dev/null +++ b/db/migrations/meta/0014_snapshot.json @@ -0,0 +1,1166 @@ +{ + "id": "c1e08a43-1ac3-4390-afcb-846577e335a1", + "prevId": "91f9c18e-c5ec-4514-937b-85c8b49e6920", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0015_snapshot.json b/db/migrations/meta/0015_snapshot.json new file mode 100644 index 0000000..06bc4c3 --- /dev/null +++ b/db/migrations/meta/0015_snapshot.json @@ -0,0 +1,1166 @@ +{ + "id": "a55fd564-26cc-4551-ab69-3aaa4a3ed2a8", + "prevId": "c1e08a43-1ac3-4390-afcb-846577e335a1", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0016_snapshot.json b/db/migrations/meta/0016_snapshot.json new file mode 100644 index 0000000..4b8cc39 --- /dev/null +++ b/db/migrations/meta/0016_snapshot.json @@ -0,0 +1,1166 @@ +{ + "id": "7db4b8f0-5d8b-4c42-8e9f-f5f288c3bc9c", + "prevId": "a55fd564-26cc-4551-ab69-3aaa4a3ed2a8", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0017_snapshot.json b/db/migrations/meta/0017_snapshot.json new file mode 100644 index 0000000..b639382 --- /dev/null +++ b/db/migrations/meta/0017_snapshot.json @@ -0,0 +1,1166 @@ +{ + "id": "b049b7cd-0181-4a37-9453-5e1a9bc79c92", + "prevId": "7db4b8f0-5d8b-4c42-8e9f-f5f288c3bc9c", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0018_snapshot.json b/db/migrations/meta/0018_snapshot.json new file mode 100644 index 0000000..dea3455 --- /dev/null +++ b/db/migrations/meta/0018_snapshot.json @@ -0,0 +1,1166 @@ +{ + "id": "d6bee4e7-1eb9-4e32-8fc0-d9c460d4dccc", + "prevId": "b049b7cd-0181-4a37-9453-5e1a9bc79c92", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0019_snapshot.json b/db/migrations/meta/0019_snapshot.json new file mode 100644 index 0000000..546b92e --- /dev/null +++ b/db/migrations/meta/0019_snapshot.json @@ -0,0 +1,1182 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "16bf6ca9-24b1-4dcf-b7aa-fb120dc178d8", + "prevId": "d6bee4e7-1eb9-4e32-8fc0-d9c460d4dccc", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0020_snapshot.json b/db/migrations/meta/0020_snapshot.json new file mode 100644 index 0000000..e63fa27 --- /dev/null +++ b/db/migrations/meta/0020_snapshot.json @@ -0,0 +1,1190 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "828e6dff-6ddf-4c50-9a5b-0a865fc6745f", + "prevId": "16bf6ca9-24b1-4dcf-b7aa-fb120dc178d8", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0021_snapshot.json b/db/migrations/meta/0021_snapshot.json new file mode 100644 index 0000000..0bd4d60 --- /dev/null +++ b/db/migrations/meta/0021_snapshot.json @@ -0,0 +1,1190 @@ +{ + "id": "31c61ea0-f1ba-461e-865c-02508b36c1ac", + "prevId": "828e6dff-6ddf-4c50-9a5b-0a865fc6745f", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0022_snapshot.json b/db/migrations/meta/0022_snapshot.json new file mode 100644 index 0000000..36d1e92 --- /dev/null +++ b/db/migrations/meta/0022_snapshot.json @@ -0,0 +1,1190 @@ +{ + "id": "1a711748-a372-470e-957e-00db96cda2e8", + "prevId": "31c61ea0-f1ba-461e-865c-02508b36c1ac", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 326843a..482e363 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -64,6 +64,104 @@ "when": 1788157642204, "tag": "0008_scenario-victim-profiles", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1788194839194, + "tag": "0009_alibi-timeline-events", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1788216627374, + "tag": "0010_victim-findings", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1788231785231, + "tag": "0011_scenario-current-authoring-upgrade", + "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1788251670559, + "tag": "0012_evidence-detail-and-death-time", + "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1788253932764, + "tag": "0013_clash-materials", + "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1788259032946, + "tag": "0014_scenario-latest-authoring-upgrade", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1788259708851, + "tag": "0015_scenario-title-rethink", + "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1788259963969, + "tag": "0016_scenario-title-balance", + "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1788259988556, + "tag": "0017_scenario-title-balance-data", + "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1788262996112, + "tag": "0018_scenario-required-records", + "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1788361336006, + "tag": "0019_investigable-places", + "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1788362994843, + "tag": "0020_deadline-disclosure", + "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1788378860077, + "tag": "0021_scenario-investigation-upgrade", + "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1788379031994, + "tag": "0022_scenario-place-public-copy-safety", + "breakpoints": true } ] } \ No newline at end of file diff --git a/db/place.ts b/db/place.ts new file mode 100644 index 0000000..9ac81fd --- /dev/null +++ b/db/place.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' +import { type VictimFinding, victimFindingSchema } from './victim-finding' + +/** + * 調べられる場所を焼いた形。 + * + * authoring 側(`db/scenario-definition.ts` の `scenarioPlaceSchema`)から + * 所見だけを抜いたもの。所見が別なのは、あれが調べて初めて分かるもので、 + * 公開側のテーブルに置けないため(遺体の findings とまったく同じ分け方)。 + * + * 顔料を持たない。色の付いた相手は答え、灰のままの相手は答えない、という区別を + * 盤面の色だけで付けるので、場所に色を与えるとその区別が消える。 + */ +export const investigablePlaceSchema = z.object({ + id: z.string().nonempty(), + name: z.string().nonempty(), + /** 名札や記録の見出しに使う短い名前。 */ + shortName: z.string().nonempty(), + /** 支度の名簿に出る紹介文。人物の publicIntroduction に当たる。 */ + introduction: z.string().nonempty(), + /** 調べているあいだ、名札の下に出る一行。所見ではなく、見れば分かる佇まい。 */ + situation: z.string().nonempty(), +}) + +export type InvestigablePlace = z.infer + +/** + * 場所ごとの所見。真相側の列に入る形。 + * + * 場所そのもの(`InvestigablePlace`)と別の列に分けてあるので、どの場所の所見かを + * ID で持ち直す必要がある。ここの `placeId` は authoring のローカル ID そのままで、 + * 実行時も同じ文字列で突き合わせる(部屋 ID と同じ扱い。uuid へは振り替えない)。 + */ +export const placeFindingsSchema = z.object({ + placeId: z.string().nonempty(), + findings: z.array(victimFindingSchema), +}) + +export type PlaceFindings = z.infer + +/** + * 保存されている値を場所の一覧として読む。 + * + * 列そのものは JSON なので、この列より前に焼かれた行や、形の変わった行が入り得る。 + * 読めないものは「場所の無い事件」として返す——ここで投げると、場所が一つ壊れただけで + * 事件そのものが開けなくなる(見取り図の parseFloorPlan と同じ判断)。 + */ +export const parseInvestigablePlaces = (value: unknown): InvestigablePlace[] => { + const parsed = z.array(investigablePlaceSchema).safeParse(value) + + return parsed.success ? parsed.data : [] +} + +/** その場所の所見。載っていない場所には所見が無い。 */ +export const findingsOfPlace = (all: PlaceFindings[], placeId: string): VictimFinding[] => { + const found = all.find((entry) => entry.placeId === placeId) + + return found === undefined ? [] : found.findings +} diff --git a/db/scenario-definition.ts b/db/scenario-definition.ts index ef4b1fa..0b8e003 100644 --- a/db/scenario-definition.ts +++ b/db/scenario-definition.ts @@ -19,28 +19,152 @@ export const scenarioMetaSchema = z.object({ category: nonemptyTextSchema.max(50), difficulty: z.int().min(1).max(5), estimatedMinutes: z.int().min(5).max(30), - tags: z.array(nonemptyTextSchema.max(50)).default([]), +}) + +/** + * 被害者を指すときの ID。 + * + * 被害者は `characters` に居ないので固有のIDを持たない。証拠や啓示の出どころとして + * 名指しするときだけ、この決め打ちの文字列を使う。指す先は一人しか居ないので、 + * 照合すべき一覧も無い。 + */ +export const VICTIM_ID = 'victim' + +/** + * 遺体と現場から分かること。 + * + * 1件1文。人物の心情や動機の解釈は書かない——あれは `revelations` の仕事で、 + * ここに混ぜると「遺体を見ただけで動機が分かる」ことになってしまう。 + * ここに書けるのは、その場で目にできるものだけ。 + * + * 調べられる場所(`scenarioPlaceSchema`)の所見も同じ形を使う。段階的に見せる仕組みまで + * 含めて同じものなので、別に定義すると片方だけ直された日に食い違う。 + */ +export const scenarioVictimFindingSchema = z.object({ + id: localIdSchema, + statement: nonemptyTextSchema, + /** 段階的に見せたいときだけ。形は revelation の解禁前提と同じ。 */ + requires: z + .object({ + revelations: z.array(localIdSchema).default([]), + evidences: z.array(localIdSchema).default([]), + }) + .default({ revelations: [], evidences: [] }), +}) + +/** + * 調べられる場所の ID。 + * + * ask の相手は人物・遺体・場所が同じ一つの口へ来るので、三者が形で見分けられないと + * 「誰を指したのか」が決まらない。人物は uuid、遺体は `VICTIM_ID` 固定なので、 + * 場所はそのどちらとも重ならないよう小文字の識別子に縛る。 + * + * 見取り図の部屋IDと同じ値を使ってよい——同じ場所を指しているなら、それは一つの場所である。 + * `type: location` のソースは部屋と場所のどちらにも当たる。 + */ +export const placeIdSchema = localIdSchema + .regex(/^[a-z][a-z0-9-]*$/, { + message: '場所の ID は英小文字で始まり、英小文字・数字・ハイフンだけで書いてください。', + }) + .refine((id) => id !== VICTIM_ID, { + message: `場所の ID に「${VICTIM_ID}」は使えません(遺体を指す ID と重なります)。`, + }) + /* + uuid も弾く。16進とハイフンだけの ID は上の形に当てはまってしまうので、 + ここで落とさないと「人物の ID に見える場所」を書けてしまう。 + */ + .refine((id) => !z.uuid().safeParse(id).success, { + message: '場所の ID に uuid の形は使えません(人物を指す ID と重なります)。', + }) + +/** + * 調べられる場所。 + * + * 遺体の二人目。喋らないが調べられる相手で、findings の組みも解禁の前提もそのまま同じ。 + * 違うのは死んでいないことだけ——だから死因も発見時刻も持たず、代わりに + * 「いま見るとどうなっているか」の一行(`situation`)を持つ。 + * + * 顔料も、アリバイ表の列も持たない。場所は動かないので、時刻軸に引く線がない。 + */ +export const scenarioPlaceSchema = z.object({ + id: placeIdSchema, + name: nonemptyTextSchema.max(20), + /** 名札や記録の見出しに使う短い名前。「帳場」「奥の間」。 */ + shortName: nonemptyTextSchema.max(8), + /** 支度の名簿に出る紹介文。人物の `publicIntroduction` に当たる。 */ + introduction: nonemptyTextSchema.max(60), + /** + * 調べているあいだ、名札の下に出る一行。 + * + * 所見ではない。「閉店の片づけが、途中で止まっている」のような、見れば誰でも分かる佇まい。 + * 調べる前から公開してよい範囲で書くこと(プレイ前の名簿にも出る値と同じ扱い)。 + */ + situation: nonemptyTextSchema.max(60), + /** + * 調べて分かること。空の場所は書けない——調べても何も出ない相手を並べると、 + * 一手ぶんの質問が無駄になるだけなので。 + */ + findings: z.array(scenarioVictimFindingSchema).min(1), }) export const scenarioVictimSchema = z.object({ name: nonemptyTextSchema.max(50), /** 肩書きひとつぶんの短い紹介。「青雨堂店主」のように、役割が分かれば足りる。 */ introduction: nonemptyTextSchema.max(60), + /* + * ここから下は、遺体を調べて初めて画面に出るもの。 + * すべて省略可にしてあるのは、この機能より前に書かれたシナリオを落とさないため。 + * 一つも無いシナリオでは、被害者は聞き込みの相手に並ばない。 + */ + /** 発見時刻。`HH:mm` で、timeline と同じ書き方。 */ + foundAt: timelineAtSchema.optional(), + /** 発見場所。画面にそのまま出る文字。部屋IDは `foundRoom` のほうへ。 */ + foundIn: nonemptyTextSchema.max(20).optional(), + /** 発見場所の部屋ID。見取り図のある事件でだけ書ける。 */ + foundRoom: localIdSchema.optional(), + /** + * 死亡推定時刻。発見時刻(`foundAt`)とは別物で、アリバイ表を横断する刻限の線になる。 + * 時刻の偽装を核にする事件では、ここが盤面の中心になる。 + */ + estimatedDeathAt: timelineAtSchema.optional(), + causeOfDeath: nonemptyTextSchema.max(100).optional(), + findings: z.array(scenarioVictimFindingSchema).default([]), }) export const scenarioFactSchema = z.object({ id: localIdSchema, statement: nonemptyTextSchema, kind: z.enum(['observation', 'physical', 'testimony', 'motive', 'truth', 'other']).optional(), - secret: z.boolean().default(false), }) export const scenarioTimelineEventSchema = z.object({ id: localIdSchema, at: timelineAtSchema, - location: nonemptyTextSchema.optional(), + /** + * 在所。**画面にそのまま出る文字**なので、短い名詞句で書く。 + * 見取り図と結びつけたいときは、部屋IDを `room` のほうへ書く。 + */ + location: nonemptyTextSchema.max(20).optional(), + /** 見取り図の部屋ID。図のある事件でだけ書ける。 */ + room: localIdSchema.optional(), + /** + * その時刻に **`location` に居た人**。関わった人ではない。 + * ここに載せた人の列に、アリバイ表の線が引かれる。 + */ participants: z.array(localIdSchema).default([]), + /** + * 離れた場所から見ていた人。線は引かれない。 + * + * 「AがBを目撃する」を一つの出来事にまとめると、盤面ではBまでその場所に立つ。 + * 見ていた側はここへ置き、その人自身の居場所は別の出来事として書く。 + */ + witnesses: z.array(localIdSchema).default([]), facts: z.array(localIdSchema).min(1), + /** + * その時刻を留めた記録の名前。「受付」「忘れ傘」「通報」。 + * アリバイ表の目盛りに `19:08 受付` の形で添う。裏付けのある出来事にだけ書く。 + */ + record: nonemptyTextSchema.max(12).optional(), description: nonemptyTextSchema.optional(), }) @@ -58,7 +182,6 @@ export const scenarioLieSchema = z.object({ export const scenarioMemorySchema = z.object({ id: localIdSchema, - about: localIdSchema, detail: nonemptyTextSchema, }) @@ -71,7 +194,6 @@ export const scenarioRelationshipSchema = z.object({ export const scenarioCharacterSchema = z.object({ id: localIdSchema, name: nonemptyTextSchema.max(100), - role: nonemptyTextSchema.max(50).optional(), publicIntroduction: nonemptyTextSchema.max(300), personality: nonemptyTextSchema, goals: z.array(nonemptyTextSchema), @@ -83,7 +205,8 @@ export const scenarioCharacterSchema = z.object({ }) export const scenarioRevelationSourceSchema = z.object({ - type: z.enum(['character', 'location']), + // victim のとき id は VICTIM_ID 固定。指す先が一人しか居ないので照合先の一覧を持たない。 + type: z.enum(['character', 'location', 'victim']), id: localIdSchema, revealCondition: nonemptyTextSchema, requires: z @@ -123,7 +246,7 @@ export const scenarioRevelationSchema = z.object({ * 難易度モードの「この人にあと N 件」を数えるのに使う。 */ export const scenarioEvidenceSourceSchema = z.object({ - type: z.enum(['character', 'location']), + type: z.enum(['character', 'location', 'victim']), id: localIdSchema, }) @@ -131,10 +254,7 @@ export const scenarioEvidenceSchema = z.object({ id: localIdSchema, label: nonemptyTextSchema.max(100), description: nonemptyTextSchema.optional(), - reveal: z.object({ - mode: z.enum(['conversation']).default('conversation'), - condition: nonemptyTextSchema, - }), + reveal: z.object({ condition: nonemptyTextSchema }), /** * 空でも通す。場所にも人物にも紐づかない証拠は、残り件数の内訳には出ないが * 総数には数えられる。 @@ -142,6 +262,19 @@ export const scenarioEvidenceSchema = z.object({ sources: z.array(scenarioEvidenceSourceSchema).default([]), supports: z.array(localIdSchema).default([]), contradicts: z.array(nonemptyTextSchema).default([]), + /** + * この証拠を掴んだら、死亡推定時刻(`victim.estimatedDeathAt`)を盤面に出すか。 + * + * 既定は false。印がひとつも無い事件では、刻限はプレイの最後まで「不明」のまま出る + * ——記録に書いてあるのは発見時刻だけで、死亡推定は手に入れて初めて分かるものだから + * (docs/design/deadline-window.md「何が窓を締めるか」)。 + * + * 所見(findings)ではなく証拠の側に置いてある。開示済みかどうかをサーバが数えられるのは + * 証拠と啓示だけで、所見には「見せてよいか」の前提があるだけ——読んだという記録が + * どこにも残らないので、印を付けても開いたかどうかを判定できない。 + * 遺体の検分から開かせたいときは `sources: { type: victim }` の証拠へ印を付ける。 + */ + revealsDeathTime: z.boolean().default(false), }) export const scenarioSolutionSchema = z.object({ @@ -154,26 +287,9 @@ export const scenarioSolutionSchema = z.object({ */ method: nonemptyTextSchema, motive: nonemptyTextSchema, - requiredFacts: z.array(localIdSchema).min(1), secretKeywords: z.array(nonemptyTextSchema).min(1), }) -export const scenarioQualitySchema = z.object({ - expectedQuestionCount: z - .object({ - min: z.int().min(0), - max: z.int().min(0), - }) - .optional(), - requiredEvidence: z - .object({ - min: z.int().min(0), - }) - .optional(), - redHerrings: z.array(localIdSchema).default([]), - notes: nonemptyTextSchema.optional(), -}) - const duplicateIndexes = (ids: string[]): number[] => { const counts = new Map() @@ -214,6 +330,13 @@ export const scenarioDefinitionShapeSchema = z.object({ * 殺人以外の事件を書けるようにするため任意にしてある。 */ victim: scenarioVictimSchema.optional(), + /** + * 調べられる場所。 + * + * 空を既定にしてあるのは、場所を持たない事件が既にあるため。人物と遺体だけで + * 成立している事件に、後から現場を足す義務を負わせない。 + */ + places: z.array(scenarioPlaceSchema).default([]), briefing: nonemptyTextSchema, floorPlan: floorPlanSchema.nullable(), facts: z.array(scenarioFactSchema).min(1), @@ -222,7 +345,6 @@ export const scenarioDefinitionShapeSchema = z.object({ revelations: z.array(scenarioRevelationSchema).default([]), evidences: z.array(scenarioEvidenceSchema), solution: scenarioSolutionSchema, - quality: scenarioQualitySchema.default({ redHerrings: [] }), }) export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefine( @@ -232,9 +354,39 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin const characterIds = new Set(scenario.characters.map((character) => character.id)) const evidenceIds = new Set(scenario.evidences.map((evidence) => evidence.id)) const revelationIds = new Set(scenario.revelations.map((revelation) => revelation.id)) - const locationIds = new Set( - scenario.floorPlan === null ? [] : scenario.floorPlan.rooms.map((room) => room.id), - ) + /* + `type: location` が指せる先。図面の部屋と、調べられる場所の両方。 + 図を持たない事件でも場所は置けるので(`floorPlan: null` の事件がある)、 + 部屋だけを照合先にすると、そこへ証拠を紐づけた瞬間に落ちる。 + 両方に同じ ID があるときは同じ場所を指しているものとして扱う。 + */ + const locationIds = new Set([ + ...(scenario.floorPlan === null ? [] : scenario.floorPlan.rooms.map((room) => room.id)), + ...scenario.places.map((place) => place.id), + ]) + + /** + * 出どころが実在するか。駄目なら理由を返す。 + * + * 被害者だけは照合すべき一覧を持たない(事件に一人しか居ない)ので、 + * 「その事件に被害者が居るか」と「決め打ちのIDか」の二点だけを見る。 + */ + const sourceIssue = (source: { type: string; id: string }): string | undefined => { + if (source.type === 'victim') { + if (scenario.victim === undefined) { + return '被害者の居ない事件で type: victim は使えません。' + } + + return source.id === VICTIM_ID + ? undefined + : `type: victim の id は「${VICTIM_ID}」で固定です。` + } + + const exists = + source.type === 'character' ? characterIds.has(source.id) : locationIds.has(source.id) + + return exists ? undefined : `存在しない ${source.type}「${source.id}」を参照しています。` + } const addDuplicateIssues = ( ids: string[], @@ -276,6 +428,61 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin (index) => ['revelations', index, 'id'], ) + /** + * 所見の検査。遺体と場所で同じものを使う。 + * + * 見るのは ID の重複と、解禁前提の参照先。証拠や啓示を書き換えたときに、 + * 調べる側だけ古い ID が残るのを防ぐ。 + */ + const addFindingIssues = ( + findings: { id: string; requires: { revelations: string[]; evidences: string[] } }[], + pathTo: (index: number) => (string | number)[], + ) => { + addDuplicateIssues( + findings.map((finding) => finding.id), + 'finding', + (index) => [...pathTo(index), 'id'], + ) + + findings.forEach((finding, findingIndex) => { + finding.requires.evidences.forEach((evidenceId, at) => { + if (!evidenceIds.has(evidenceId)) { + ctx.addIssue({ + code: 'custom', + path: [...pathTo(findingIndex), 'requires', 'evidences', at], + message: `存在しない evidence「${evidenceId}」を参照しています。`, + }) + } + }) + + finding.requires.revelations.forEach((revelationId, at) => { + if (!revelationIds.has(revelationId)) { + ctx.addIssue({ + code: 'custom', + path: [...pathTo(findingIndex), 'requires', 'revelations', at], + message: `存在しない revelation「${revelationId}」を参照しています。`, + }) + } + }) + }) + } + + addFindingIssues(scenario.victim === undefined ? [] : scenario.victim.findings, (index) => [ + 'victim', + 'findings', + index, + ]) + + addDuplicateIssues( + scenario.places.map((place) => place.id), + 'place', + (index) => ['places', index, 'id'], + ) + + scenario.places.forEach((place, placeIndex) => { + addFindingIssues(place.findings, (index) => ['places', placeIndex, 'findings', index]) + }) + const lieEntries = scenario.characters.flatMap((character, characterIndex) => character.lies.map((lie, lieIndex) => ({ characterIndex, lie, lieIndex })), ) @@ -333,16 +540,6 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin }) } - character.memories.forEach((memory, memoryIndex) => { - if (!factIds.has(memory.about)) { - ctx.addIssue({ - code: 'custom', - path: ['characters', characterIndex, 'memories', memoryIndex, 'about'], - message: `存在しない fact「${memory.about}」を参照しています。`, - }) - } - }) - character.relationships.forEach((relationship, relationshipIndex) => { if (!characterIds.has(relationship.character)) { ctx.addIssue({ @@ -365,6 +562,36 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin } }) + event.witnesses.forEach((characterId, witnessIndex) => { + if (!characterIds.has(characterId)) { + ctx.addIssue({ + code: 'custom', + path: ['timeline', eventIndex, 'witnesses', witnessIndex], + message: `存在しない character「${characterId}」を参照しています。`, + }) + } + + /* + 同じ人を両方に載せると、その人はその場に居たのか離れて見ていたのか決まらない。 + 決まらないまま線を引くと、盤面に「居なかった場所に立っている人」が現れる。 + */ + if (event.participants.includes(characterId)) { + ctx.addIssue({ + code: 'custom', + path: ['timeline', eventIndex, 'witnesses', witnessIndex], + message: `「${characterId}」が participants と witnesses の両方にいます。居た場所はどちらか一方です。`, + }) + } + }) + + if (event.room !== undefined && !locationIds.has(event.room)) { + ctx.addIssue({ + code: 'custom', + path: ['timeline', eventIndex, 'room'], + message: `存在しない部屋「${event.room}」を参照しています。`, + }) + } + event.facts.forEach((factId, factIndex) => { if (!factIds.has(factId)) { ctx.addIssue({ @@ -376,6 +603,14 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin }) }) + if (scenario.victim?.foundRoom !== undefined && !locationIds.has(scenario.victim.foundRoom)) { + ctx.addIssue({ + code: 'custom', + path: ['victim', 'foundRoom'], + message: `存在しない部屋「${scenario.victim.foundRoom}」を参照しています。`, + }) + } + const hasClockTime = scenario.timeline.some((event) => CLOCK_TIME_RE.test(event.at)) const hasIsoDateTime = scenario.timeline.some((event) => ISO_DATETIME_RE.test(event.at)) if (hasClockTime && hasIsoDateTime) { @@ -413,14 +648,13 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin }) revelation.sources.forEach((source, sourceIndex) => { - const sourceExists = - source.type === 'character' ? characterIds.has(source.id) : locationIds.has(source.id) + const issue = sourceIssue(source) - if (!sourceExists) { + if (issue !== undefined) { ctx.addIssue({ code: 'custom', path: ['revelations', revelationIndex, 'sources', sourceIndex, 'id'], - message: `存在しない ${source.type}「${source.id}」を参照しています。`, + message: issue, }) } @@ -515,14 +749,13 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin // 場所IDは見取り図の部屋IDと文字列で一致しているだけなので、 // 片方を書き換えた瞬間に証拠がどこにも紐づかなくなる。ここで落とす。 evidence.sources.forEach((source, sourceIndex) => { - const sourceExists = - source.type === 'character' ? characterIds.has(source.id) : locationIds.has(source.id) + const issue = sourceIssue(source) - if (!sourceExists) { + if (issue !== undefined) { ctx.addIssue({ code: 'custom', path: ['evidences', evidenceIndex, 'sources', sourceIndex, 'id'], - message: `存在しない ${source.type}「${source.id}」を参照しています。`, + message: issue, }) } }) @@ -537,6 +770,18 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin } }) + /* + 死亡推定時刻を持たない事件で印だけ立てても、開示できる時刻がどこにも無い。 + 作者は「開いたはずなのに盤面が変わらない」ものを見ることになるので、ここで落とす。 + */ + if (evidence.revealsDeathTime && scenario.victim?.estimatedDeathAt === undefined) { + ctx.addIssue({ + code: 'custom', + path: ['evidences', evidenceIndex, 'revealsDeathTime'], + message: 'victim.estimatedDeathAt の無い事件では revealsDeathTime を立てられません。', + }) + } + evidence.contradicts.forEach((reference, contradictIndex) => { const lieId = reference.startsWith('lie:') ? reference.slice(4) : undefined const referencesKnownLie = lieId === undefined ? false : lieIdSet.has(lieId) @@ -558,23 +803,18 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin }) } - scenario.solution.requiredFacts.forEach((factId, factIndex) => { - if (!factIds.has(factId)) { - ctx.addIssue({ - code: 'custom', - path: ['solution', 'requiredFacts', factIndex], - message: `存在しない fact「${factId}」を参照しています。`, - }) - } - }) - const publicText = [ scenario.meta.title, scenario.meta.synopsis, scenario.meta.category, - ...scenario.meta.tags, scenario.briefing, ...scenario.characters.map((character) => character.publicIntroduction), + /* + 場所の名前・紹介・佇まいも公開情報。名簿には調べる前から並ぶし、 + 佇まいは調べ始めた瞬間に名札の下へ出る。所見(findings)は調べて初めて + 出るものなので、遺体の所見と同じくここには入れない。 + */ + ...scenario.places.flatMap((place) => [place.name, place.introduction, place.situation]), ] .join('\n') .toLocaleLowerCase() @@ -588,18 +828,11 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin }) } }) - - const expectedQuestions = scenario.quality.expectedQuestionCount - if (expectedQuestions !== undefined && expectedQuestions.min > expectedQuestions.max) { - ctx.addIssue({ - code: 'custom', - path: ['quality', 'expectedQuestionCount'], - message: 'expectedQuestionCount.min は max 以下でなければなりません。', - }) - } }, ) +export type ScenarioFinding = z.infer +export type ScenarioPlace = z.infer export type ScenarioEvidenceSource = z.infer export type ScenarioRevelationSource = z.infer export type ScenarioRevelation = z.infer diff --git a/db/scenarios/avalanche-lodge-phantom-room.yaml b/db/scenarios/avalanche-lodge-phantom-room.yaml index bd089de..62db420 100644 --- a/db/scenarios/avalanche-lodge-phantom-room.yaml +++ b/db/scenarios/avalanche-lodge-phantom-room.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: avalanche-lodge-phantom-room meta: - title: 雪崩の山荘、白樺峰の夜 + title: "白樺峰、明朝まで" synopsis: "午後九時四十五分、雪崩で道路を失った白樺峰ホテルの支配人室で、支配人の早瀬隆司が死亡しているのが見つかりました。外部との電話も不通で、事件時刻に館内にいた従業員は三人だけです。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [山荘, 雪崩, 偽の呼び出し, アリバイ] victim: name: 早瀬隆司 introduction: 白樺峰ホテル支配人 + foundAt: 21:45 + foundIn: 支配人室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 早瀬隆司は支配人室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「翌朝の会計監査メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,7 +48,6 @@ facts: - id: natsume-knew-renumbering statement: 夏目悠は三年前の改装時に客室番号変更の案内を担当しており、二一四号室が存在しないことを知っていた kind: truth - secret: true - id: fake-message-left statement: 21時16分ごろ、フロントの伝言台に「二一四号室、暖房停止、至急確認」と書かれた紙が置かれた kind: physical @@ -60,27 +66,21 @@ facts: - id: hayase-death-2130 statement: 21時30分ごろ、早瀬隆司は支配人室で襲われ死亡した kind: truth - secret: true - id: natsume-killed-hayase statement: 夏目悠は21時30分ごろ支配人室で早瀬隆司を襲い死亡させた kind: truth - secret: true - id: hayase-audit-next-morning statement: 早瀬隆司は翌朝、夜勤時の現金不足について夏目悠へ正式な監査を行う予定だった kind: motive - secret: true - id: natsume-cash-shortage statement: 夏目悠は夜勤売上の不足を私的な立替で一時的に隠していた kind: motive - secret: true - id: oda-hid-broken-linen statement: 小田真紀は高価なリネンを誤って破損し、交換記録を書き換えていた kind: other - secret: true - id: fujino-took-wine statement: 藤野修平は廃棄予定ではない料理用ワインを自宅用に持ち帰ろうとしていた kind: other - secret: true - id: paper-from-night-pad statement: 二一四号室の伝言紙は、夏目悠が夜勤日誌に使う個人管理のメモ束と同じ裁断痕を持つ kind: physical @@ -93,40 +93,47 @@ timeline: participants: [natsume] facts: [hayase-audit-next-morning, natsume-cash-shortage] description: 早瀬が夏目に、翌朝の夜勤会計監査を告げる。 + location: 山荘内 - id: fake-message at: "21:16" participants: [natsume] facts: [fake-message-left, no-214-call, paper-from-night-pad] + record: 伝言紙 description: フロントに二一四号室の設備苦情を装う伝言が置かれる。 + location: フロント - id: ask-oda at: "21:24" participants: [natsume, oda] facts: [natsume-asked-oda] description: 夏目が小田に二一四号室の場所を尋ね、客室棟へ向かうように見せる。 + location: 客室棟 - id: management-corridor at: "21:27" participants: [natsume, oda] facts: [oda-saw-natsume-office-side] description: 小田が管理廊下から出てくる夏目を目撃する。 + location: 管理廊下 - id: hayase-death at: "21:30" participants: [natsume] facts: [hayase-death-2130, natsume-killed-hayase] description: 夏目が支配人室で早瀬を襲い、早瀬は死亡する。 + location: 支配人室 - id: ask-fujino at: "21:31" participants: [natsume, fujino] facts: [natsume-asked-fujino] description: 夏目が厨房前で藤野にも二一四号室の場所を尋ねる。 + location: 厨房前 - id: discovery at: "21:45" participants: [fujino, natsume, oda] facts: [body-found-2145] description: 藤野が支配人室で早瀬の死を発見する。 + location: 支配人室 characters: - id: natsume name: 夏目悠 - role: suspect publicIntroduction: "丁寧で機転が利く夜勤責任者。" personality: 丁寧で機転が利く夜勤責任者。館内事情には誰より詳しいが、会計の話になると慎重に言葉を選ぶ。早瀬には長年世話になった一方、翌朝の監査を強く恐れていた。 goals: @@ -153,10 +160,8 @@ characters: strategy: maintain-until-contradicted memories: - id: renovation-numbering - about: natsume-knew-renumbering detail: 改装時、二一四号室を廃止する案内を自分で各予約サイトへ登録したので、その番号だけは忘れようがない。 - id: audit-fear - about: hayase-audit-next-morning detail: 早瀬に「明日の朝、帳尻を全部見せてもらう」と静かに言われたとき、逃げ道がなくなったと感じた。 relationships: - character: oda @@ -167,7 +172,6 @@ characters: attitude: 深く考えず話す性格なので利用しやすいと思っている - id: oda name: 小田真紀 - role: witness publicIntroduction: "几帳面で、人の出入りをよく覚えている客室係。" personality: 几帳面で、人の出入りをよく覚えている客室係。自分の備品管理ミスを隠しているため、管理記録を調べられることには抵抗がある。 goals: @@ -184,7 +188,6 @@ characters: strategy: maintain-until-contradicted memories: - id: saw-management-corridor - about: oda-saw-natsume-office-side detail: 客室棟へ行ったはずの夏目が管理廊下から出てきたので、「二一四号室はそっちじゃない」と思った。 relationships: - character: natsume @@ -192,7 +195,6 @@ characters: attitude: 普段は頼れるが、数字の話を避けるところが気になっている - id: fujino name: 藤野修平 - role: witness publicIntroduction: "気さくで話好きな料理人。" personality: 気さくで話好きな料理人。細かな時刻には弱いが、厨房前で誰と話したかはよく覚えている。持ち帰ろうとした料理用ワインの件だけは触れられたくない。 goals: @@ -209,7 +211,6 @@ characters: strategy: maintain-until-contradicted memories: - id: natsume-asked-again - about: natsume-asked-fujino detail: 夏目は二一四号室が分からないと言ったが、長年いる人がそんな番号を忘れるかな、と少し引っかかった。 relationships: - character: natsume @@ -257,7 +258,6 @@ evidences: label: 改装後の客室番号表 description: 三年前の改装で二一四号室は廃止され、その案内担当者欄には夏目の名前がある。 reveal: - mode: conversation condition: 小田か夏目に二一四号室の改装履歴と、誰が番号変更を担当したか尋ねたら開示する。 sources: - type: character @@ -270,7 +270,6 @@ evidences: label: 館内電話の着信一覧 description: 二一四号室を名乗る着信はなく、設備苦情は電話ではなく紙の伝言だけで現れている。 reveal: - mode: conversation condition: 夏目に設備苦情を誰からどの電話で受けたのか確認するか、小田にフロントの着信記録について尋ねたら開示する。 sources: - type: character @@ -283,7 +282,6 @@ evidences: label: 伝言紙の裁断痕 description: 二一四号室の伝言紙は、夏目が夜勤日誌で使うメモ束から切り離された紙と一致する。 reveal: - mode: conversation condition: 二一四号室の伝言紙を誰が扱えるか、または夏目の夜勤メモについて追及したら開示する。 sources: - type: character @@ -296,7 +294,6 @@ evidences: label: 二十一時二十七分の管理廊下目撃 description: 小田は、客室棟を探しているはずの夏目が支配人室へ続く管理廊下から出てくるのを見ている。 reveal: - mode: conversation condition: 小田に21時20分から35分の間に誰を見たか尋ね、管理廊下での目撃を確認したら開示する。 sources: - type: character @@ -307,20 +304,20 @@ evidences: label: 翌朝の会計監査メモ description: 早瀬の予定表には、翌朝最初の業務として夏目の夜勤売上不足を確認する予定が記されている。 reveal: - mode: conversation - condition: 夏目か小田に早瀬が翌朝予定していた会計確認について尋ねたら開示する。 + condition: 夏目か小田に早瀬が翌朝予定していた会計確認について尋ねたら開示する。または遺体・現場を調べ、「翌朝の会計監査メモ」に関わる資料を確認したら開示する。 sources: - type: character id: natsume - type: character id: oda + - type: victim + id: victim supports: [hayase-audit-next-morning, natsume-cash-shortage] contradicts: [] - id: linen-record label: 書き換えられたリネン交換記録 description: 小田が破損品の処理を隠すため、一部の交換記録を書き換えていたことが分かる。事件とは独立した隠し事である。 reveal: - mode: conversation condition: 小田に備品管理記録の修正について具体的に尋ねたら開示する。 sources: - type: character @@ -331,7 +328,6 @@ evidences: label: 厨房裏の持ち帰り袋 description: 藤野が料理用ワインを入れた袋が見つかるが、事件時刻の支配人室とは関係しない。 reveal: - mode: conversation condition: 藤野に厨房から私物として持ち出そうとしたものがないか追及したら開示する。 sources: - type: character @@ -343,18 +339,9 @@ solution: summary: 犯人は夏目悠。夏目は三年前の改装案内を担当しており、二一四号室が存在しないことを知っていた。それにもかかわらず、二一四号室の設備苦情という伝言を用意し、小田と藤野の前でわざと場所を尋ねて、自分が客室棟を探し回っている印象を作った。実際には21時27分ごろ小田が支配人室側の管理廊下から出てくる夏目を目撃しており、21時30分ごろ夏目は支配人室で早瀬を襲った。翌朝には夏目が隠していた夜勤売上の不足について監査が予定されていた。電話記録に二一四号室からの着信がなく、伝言紙も夏目の夜勤メモと一致するため、存在しない部屋を探す行動自体が見せるためのアリバイ工作だったと分かる。 method: 存在しない客室からの苦情を装って館内を探し回る姿を複数人に見せた後、支配人室へ戻って早瀬を襲い、再び人前に出てアリバイを補強した motive: 翌朝の会計監査で、夜勤売上の不足を隠していたことが発覚するのを恐れたため - requiredFacts: [room-214-removed, natsume-knew-renumbering, no-214-call, oda-saw-natsume-office-side, paper-from-night-pad, hayase-audit-next-morning, natsume-killed-hayase] secretKeywords: - 犯人は夏目 - 夏目が犯人 - 夏目が早瀬を襲 - 私が早瀬を襲 - 二一四号室を使ってアリバイ -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [oda-hid-broken-linen, fujino-took-wine] - notes: 偽の呼び出しそのものより、「長年勤務する夏目が存在しない部屋を知らないはずがない」という知識の矛盾が主軸。電話記録、伝言紙、管理廊下目撃を重ねて初めて工作と犯行機会が結びつく。 diff --git a/db/scenarios/avalanche-monastery-bell-window.yaml b/db/scenarios/avalanche-monastery-bell-window.yaml index 007dce6..1e84994 100644 --- a/db/scenarios/avalanche-monastery-bell-window.yaml +++ b/db/scenarios/avalanche-monastery-bell-window.yaml @@ -1,15 +1,34 @@ schemaVersion: 1 id: avalanche-monastery-bell-window meta: - title: 雪崩の修道院、山上の三人 + title: "祈りの山は雪に閉ざされる" synopsis: "午後九時四十分、雪崩で麓への道を断たれた山上の修道院資料館で、館長・高瀬静一が死亡しているのが見つかりました。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [修道院, 雪崩, 鐘, 時系列] victim: name: 高瀬静一 introduction: 修道院資料館館長 + foundAt: 21:40 + foundIn: 資料整理室 + estimatedDeathAt: "21:05" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 高瀬静一は資料整理室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「修復費の追加請求一覧」に関わる資料が残されている。 +places: + - id: choir-access + name: 聖歌席裏 + shortName: 聖歌席裏 + introduction: 鐘の修復資材が置かれた、一階の作業区画 + situation: 木箱と工事用の札がまとめて置かれている + findings: + - id: test-line-still-present + statement: 点検口の奥に仮設試験線が残り、工事タグも撤去済みの状態にはなっていない。 + - id: test-line-reaches-bell + statement: 仮設試験線は、鐘塔へ入らず一階側から鐘の作動確認を行える配線になっている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,15 +57,12 @@ facts: - id: repair-overbilling statement: 高瀬静一は事件当日、鐘塔修復費の一部に架空の追加工事が計上されていることを発見した kind: motive - secret: true - id: mizuki-overbilled statement: 水城奈央は協力業者と示し合わせ、実施していない追加工事を請求書へ載せていた kind: motive - secret: true - id: takase-confronted-mizuki statement: 20時48分ごろ、高瀬静一は水城奈央に翌朝までに追加工事の根拠を示すよう求めた kind: motive - secret: true - id: temporary-test-line-exists statement: 鐘の修復期間中、鐘塔へ入らず一階の聖歌席裏から鐘の作動確認を行える仮設試験線が設けられていた kind: physical @@ -59,11 +75,9 @@ facts: - id: mizuki-killed-takase statement: 21時05分ごろ、水城奈央は資料整理室で高瀬静一を襲い死亡させた kind: truth - secret: true - id: mizuki-rang-bell-remotely statement: 21時20分、水城奈央は聖歌席裏に残っていた仮設試験線を使って鐘を一度鳴らした kind: truth - secret: true - id: all-heard-bell statement: 21時20分、館内にいた水城奈央、黒川玲、玄田修の三人は鐘が一度鳴るのを聞いた kind: observation @@ -76,11 +90,9 @@ facts: - id: kurokawa-secret-photo statement: 黒川玲は撮影禁止の古文書を私物端末で撮影していた kind: other - secret: true - id: genda-secret-key-copy statement: 玄田修は紛失対策として無許可の予備鍵を一本作っていた kind: other - secret: true - id: body-found-2140 statement: 21時40分、黒川玲が資料整理室で高瀬静一の死を発見した kind: observation @@ -90,30 +102,35 @@ timeline: participants: [mizuki] facts: [repair-overbilling, mizuki-overbilled, takase-confronted-mizuki] description: 高瀬が水城に修復費の追加請求について説明を求める。 + location: 修道院内 - id: takase-death at: "21:05" participants: [mizuki] facts: [mizuki-killed-takase] description: 水城が資料整理室で高瀬を襲う。 + location: 資料整理室 - id: bell-rung at: "21:20" participants: [mizuki, kurokawa, genda] facts: [mizuki-rang-bell-remotely, all-heard-bell, no-one-saw-takase-bell] description: 九時二十分の鐘が鳴り、三人はいつもの高瀬の閉館合図だと思い込む。 + location: 修道院内 - id: test-line-clue at: "21:27" participants: [genda] facts: [temporary-test-line-exists, genda-thought-line-removed, test-line-tag-remained] + record: 工事タグ description: 玄田が聖歌席裏の点検口を通るが、残った工事タグを気に留めない。 + location: 聖歌席裏 - id: discovery at: "21:40" participants: [kurokawa, mizuki, genda] facts: [body-found-2140] description: 黒川が資料整理室で高瀬の死を発見する。 + location: 資料整理室 characters: - id: mizuki name: 水城奈央 - role: suspect publicIntroduction: "修復設計士。" personality: 理屈が明快な修復設計士で、建物の構造と工事手順に詳しい。鐘は高瀬が鳴らしたという前提を強調し、自分だけが仮設試験線を熟知していることには触れたがらない。 goals: @@ -140,7 +157,6 @@ characters: strategy: maintain-until-contradicted memories: - id: test-line-design - about: mizuki-knew-test-line detail: 修復中だけ使う試験線を自分で図面に引いたので、聖歌席裏から鐘を作動確認できることを誰よりよく知っている。 relationships: - character: kurokawa @@ -151,7 +167,6 @@ characters: attitude: 工事の現状より昔からの運用を信じる癖があると見ている - id: kurokawa name: 黒川玲 - role: witness publicIntroduction: "文献には厳密だが建築設備には疎い研究者。" personality: 文献には厳密だが建築設備には疎い研究者。鐘を聞いたことは確かだが、音が鳴る仕組みまでは知らない。撮影禁止史料を写したことを隠している。 goals: @@ -168,7 +183,6 @@ characters: strategy: maintain-until-contradicted memories: - id: heard-one-bell - about: all-heard-bell detail: 九時二十分に一度だけ鐘が響いたので、いつもの閉館合図だと思った。高瀬本人の姿は見ていない。 relationships: - character: mizuki @@ -179,7 +193,6 @@ characters: attitude: 館内の昔からの習慣について頼りにしている - id: genda name: 玄田修 - role: suspect publicIntroduction: "長年勤める管理人。" personality: 長年勤める管理人。鍵の扱いにはうるさいが、工事中の仮設設備には疎い。無断で予備鍵を作ったため、鍵の話題になると妙に防御的になる。 goals: @@ -196,8 +209,9 @@ characters: strategy: maintain-until-contradicted memories: - id: assumed-removed - about: genda-thought-line-removed detail: 前日の作業員が「試験は終わり」と言っていたので、仮設線そのものも外したのだと思い込んでいた。 + - id: death-estimate-memory + detail: 発見時の高瀬の状態と冷えた資料整理室の様子を確認しており、死亡は21時05分ごろと見積もられるという確認内容を覚えている。 relationships: - character: mizuki relation: 工事監理者 @@ -253,7 +267,6 @@ evidences: label: 鐘塔修復の試験系統図 description: 修復中は聖歌席裏の仮設試験線から鐘の作動確認ができることが図面に記されている。 reveal: - mode: conversation condition: 水城に鐘の修復手順を詳しく尋ねるか、玄田に塔へ入らない試験方法の有無を確認したら開示する。 sources: - type: character @@ -266,20 +279,19 @@ evidences: label: 聖歌席裏の工事タグ description: 点検口には仮設試験線がまだ接続中であることを示す工事タグが残っている。 reveal: - mode: conversation - condition: 玄田に聖歌席裏の点検口で見たものを尋ねるか、水城に仮設線の撤去状況を確認したら開示する。 + condition: 玄田に聖歌席裏の点検口で見たものを尋ねるか、水城に仮設線の撤去状況を確認したら開示する。または聖歌席裏の点検口を調べ、残った工事タグと仮設線を確認したら開示する。 sources: - type: character id: genda - type: character id: mizuki + - { type: location, id: choir-access } supports: [test-line-tag-remained, temporary-test-line-exists] contradicts: ["lie:mizuki-bell-alibi"] - id: no-visual-confirmation label: 九時二十分の目撃不在 description: 三人とも鐘を聞いているが、その時刻に高瀬本人が鐘塔へ向かう姿を見た者はいない。 reveal: - mode: conversation condition: 黒川か玄田に鐘が鳴った時刻に高瀬本人を見たか尋ねたら開示する。 sources: - type: character @@ -292,20 +304,20 @@ evidences: label: 修復費の追加請求一覧 description: 実施記録のない追加工事が水城の承認で請求され、高瀬がその項目へ印を付けている。 reveal: - mode: conversation - condition: 水城に高瀬から追及された修復費について尋ねるか、玄田に問題になっていた工事項目を確認したら開示する。 + condition: 水城に高瀬から追及された修復費について尋ねるか、玄田に問題になっていた工事項目を確認したら開示する。または遺体・現場を調べ、「修復費の追加請求一覧」に関わる資料を確認したら開示する。 sources: - type: character id: mizuki - type: character id: genda + - type: victim + id: victim supports: [repair-overbilling, mizuki-overbilled, takase-confronted-mizuki] contradicts: ["lie:mizuki-no-overbilling"] - id: forbidden-photos label: 撮影禁止史料の画像 description: 黒川の端末には撮影禁止の古文書画像が残るが、鐘の時刻とは関係しない。 reveal: - mode: conversation condition: 黒川に事件前の史料閲覧と撮影について尋ね、無断撮影を否定したら開示する。 sources: - type: character @@ -316,29 +328,30 @@ evidences: label: 無許可の予備鍵 description: 玄田が作った予備鍵が見つかるが、九時二十分の鐘は鍵を使わず鳴らせたため主経路ではない。 reveal: - mode: conversation condition: 玄田に鍵の複製について尋ね、予備鍵を否定したら開示する。 sources: - type: character id: genda supports: [genda-secret-key-copy] contradicts: ["lie:genda-no-copy-key"] + - id: death-estimate + label: 発見時の死亡推定 + description: 資料整理室の室温と発見時の状態を照合すると、高瀬が死亡したのは21時05分ごろと見積もられる。21時20分の鐘より前である。 + reveal: + condition: 遺体を調べて発見時の状態から死亡時刻を推定するか、玄田に発見時の状態と確認内容を具体的に尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: genda } + supports: [mizuki-killed-takase] + contradicts: [] + revealsDeathTime: true solution: culprit: mizuki summary: 犯人は水城奈央。修復費の架空請求を高瀬に見つけられ、翌朝までの説明を求められたことから高瀬を襲った。その後、水城は自分が設計した仮設試験線を利用し、21時20分に塔へ入らず鐘を鳴らした。三人は毎晩の習慣から高瀬本人が鐘を鳴らしたと解釈したが、誰もその時刻の高瀬を見ていない。工事系統図と聖歌席裏に残ったタグから、鐘は鍵なしでも鳴らせたと分かる。鐘の音は時刻の手掛かりではあっても、高瀬の生存証明ではなかった。 method: 高瀬を襲った後、修復工事用の仮設試験線で定刻の鐘を鳴らし、犯行時刻を九時二十分以後だと思わせた motive: 修復費の架空請求が発覚し、翌朝の説明と責任追及を恐れたため - requiredFacts: [mizuki-overbilled, takase-confronted-mizuki, temporary-test-line-exists, mizuki-knew-test-line, mizuki-killed-takase, mizuki-rang-bell-remotely, no-one-saw-takase-bell] secretKeywords: - 犯人は水城 - 水城が高瀬を襲 - 水城が試験線で鐘 - 鐘で死亡時刻を偽装 -quality: - expectedQuestionCount: - min: 10 - max: 22 - requiredEvidence: - min: 3 - redHerrings: [kurokawa-secret-photo, genda-secret-key-copy] - notes: 「鍵を持つ者しか鐘を鳴らせない」という前提を、工事中の例外で崩す。予備鍵は意図的なミスリードであり、真相の操作経路とは結びつかない。 diff --git a/db/scenarios/blizzard-lodge-seat-score-alibi.yaml b/db/scenarios/blizzard-lodge-seat-score-alibi.yaml index 761bb45..f6a50a0 100644 --- a/db/scenarios/blizzard-lodge-seat-score-alibi.yaml +++ b/db/scenarios/blizzard-lodge-seat-score-alibi.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: blizzard-lodge-seat-score-alibi meta: - title: 吹雪の研修ロッジ、四人の夜 + title: "研修はもう終わった" synopsis: "午後九時四十分、吹雪で道路が閉鎖された研修ロッジで、運営責任者・塚本誠が書斎で死亡しているのが見つかりました。" category: クローズドサークル difficulty: 4 estimatedMinutes: 15 - tags: [ロッジ, 吹雪, アリバイ, 記録] victim: name: 塚本誠 introduction: 研修ロッジ運営責任者 + foundAt: 21:40 + foundIn: 書斎 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 塚本誠は書斎で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「外部講師費の精算書」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -47,11 +54,9 @@ facts: - id: katase-left-2112 statement: 21時12分ごろ、片瀬真央は飲み物を取りに行くと言って談話室を離れた kind: observation - secret: true - id: mamiya-substituted-blue statement: 21時13分から21時24分ごろまで、間宮葵が片瀬真央の代わりに青席へ座ってゲームを続けた kind: observation - secret: true - id: blue-score-continued statement: 片瀬真央が席を離れていた間も、青席の得点は途切れず得点表へ記録された kind: physical @@ -61,19 +66,15 @@ facts: - id: payment-fraud statement: 片瀬真央は実施していない外部講師費を計上し、精算額の一部を私的に流用していた kind: motive - secret: true - id: tsukamoto-found-fraud statement: 塚本誠は事件当日の夜、片瀬真央が処理した講師費の一部に架空計上があることを発見した kind: motive - secret: true - id: tsukamoto-summoned-katase statement: 21時10分ごろ、塚本誠は片瀬真央へ書斎に来るよう短いメモを渡した kind: motive - secret: true - id: katase-killed-tsukamoto statement: 21時18分ごろ、片瀬真央は書斎で塚本誠を襲い死亡させた kind: truth - secret: true - id: todo-saw-katase-return statement: 藤堂凛は21時25分ごろ、談話室の廊下側から戻ってくる片瀬真央を見た kind: observation @@ -83,58 +84,63 @@ facts: - id: todo-secret-plagiarism statement: 藤堂凛は研修資料の一部を出典表示なしで転用していた kind: other - secret: true - id: yoshioka-secret-expense statement: 吉岡蓮は私用の交通費を研修経費へ混ぜて精算していた kind: other - secret: true - id: mamiya-secret-room statement: 間宮葵は規則に反して空き客室を私物置き場として使っていた kind: other - secret: true - id: body-found-2140 statement: 21時40分、吉岡蓮が書斎で塚本誠の死を発見した kind: observation timeline: - id: fraud-discovered at: "21:05" - participants: [katase, yoshioka] + participants: [] facts: [payment-fraud, tsukamoto-found-fraud] description: 塚本が講師費の精算書から架空計上を見つける。 + location: ロッジ内 - id: summon-note at: "21:10" participants: [katase] facts: [tsukamoto-summoned-katase] description: 塚本が片瀬へ書斎に来るようメモを渡す。 + location: 書斎 - id: katase-leaves-game at: "21:12" participants: [katase, mamiya, todo, yoshioka] facts: [katase-left-2112, mamiya-substituted-blue, game-tracks-seats] + record: 席別得点表 description: 片瀬が談話室を離れ、間宮が青席へ座ってゲームを続ける。 + location: 談話室 - id: tsukamoto-death at: "21:18" participants: [katase] facts: [katase-killed-tsukamoto] description: 片瀬が書斎で塚本を襲う。 + location: 書斎 - id: score-continues at: "21:21" participants: [mamiya, todo, yoshioka] facts: [blue-score-continued, yoshioka-copied-score-later] + record: 青席の得点 description: 片瀬不在のまま青席の得点が続き、吉岡が後から席単位で清書する。 + location: ロッジ内 - id: katase-returns at: "21:25" participants: [katase, todo, mamiya, yoshioka] facts: [katase-returned-2125, todo-saw-katase-return] description: 片瀬が談話室へ戻り、間宮から青席を引き継ぐ。 + location: 談話室 - id: discovery at: "21:40" participants: [yoshioka, katase, todo, mamiya] facts: [body-found-2140] description: 吉岡が書斎で塚本の死を発見する。 + location: 書斎 characters: - id: katase name: 片瀬真央 - role: suspect publicIntroduction: "話術に長けた企画担当。" personality: 話術に長けた企画担当。得点表に自分の名前があることを「ずっとその場にいた証明」として強調する。講師費の精算内容を詳しく聞かれると話題を変えたがる。 goals: @@ -163,7 +169,6 @@ characters: strategy: maintain-until-contradicted memories: - id: seat-substitution - about: mamiya-substituted-blue detail: 席の得点だけを残すゲームなので、誰かに数分座ってもらっても表だけ見れば交代は分からないと知っていた。 relationships: - character: todo @@ -177,7 +182,6 @@ characters: attitude: 代打で座ったことを黙っていてほしい - id: todo name: 藤堂凛 - role: witness publicIntroduction: "言葉の細部をよく覚える講師。" personality: 言葉の細部をよく覚える講師。片瀬が戻ってきた場面は覚えているが、研修資料の無断転用を隠したいので自分への追及には敏感。 goals: @@ -194,7 +198,6 @@ characters: strategy: maintain-until-contradicted memories: - id: katase-came-back - about: todo-saw-katase-return detail: 九時二十五分ごろ、片瀬が廊下から戻ってきて「ありがとう」と間宮に声をかけ、青席へ座り直した。 relationships: - character: katase @@ -208,7 +211,6 @@ characters: attitude: 片瀬の代わりに座っていたことを覚えている - id: yoshioka name: 吉岡蓮 - role: suspect publicIntroduction: "研修の事務を担当する職員。" personality: 数字を整えて記録するのが好きな事務職員。得点表はラウンドごとに後から清書しており、誰が席にいたかまでは記録していない。私用交通費の混入を隠している。 goals: @@ -225,7 +227,6 @@ characters: strategy: maintain-until-contradicted memories: - id: score-by-seat - about: yoshioka-copied-score-later detail: 得点表には青席、赤席という単位で数字を写しただけで、途中の交代者名を書く欄はなかった。 relationships: - character: katase @@ -239,7 +240,6 @@ characters: attitude: 青席の代打をしていたのを見ている - id: mamiya name: 間宮葵 - role: witness publicIntroduction: "頼まれると断れない設備係。" personality: 頼まれると断れない設備係。片瀬に少しだけ代わってと言われ、深く考えず青席へ座った。空き客室を私物置き場に使っていることを隠したい。 goals: @@ -256,7 +256,6 @@ characters: strategy: maintain-until-contradicted memories: - id: sat-blue - about: mamiya-substituted-blue detail: 片瀬に「飲み物を取ってくる間だけ」と頼まれ、青席で何ラウンドか続けた。得点は青席のまま記録された。 relationships: - character: katase @@ -316,7 +315,6 @@ evidences: label: 四色の席別得点表 description: 表は青・赤・白・黄の席ごとに得点を記録し、途中で座った人物名は残さない形式である。 reveal: - mode: conversation condition: 吉岡に得点表の付け方を尋ねるか、片瀬に青席の記録が何を意味するか問い直したら開示する。 sources: - type: character @@ -329,7 +327,6 @@ evidences: label: 青席の代打証言 description: 間宮は21時13分から21時24分ごろまで片瀬の代わりに青席へ座っていた。 reveal: - mode: conversation condition: 間宮にゲーム中の席替わりを尋ねるか、藤堂に片瀬が戻った時の場面を確認したら開示する。 sources: - type: character @@ -342,7 +339,6 @@ evidences: label: 二十一時二十五分の帰席目撃 description: 藤堂は片瀬が廊下側から談話室へ戻って青席へ座り直すところを見ている。 reveal: - mode: conversation condition: 藤堂に21時台の片瀬の出入りを尋ねたら開示する。 sources: - type: character @@ -353,20 +349,20 @@ evidences: label: 外部講師費の精算書 description: 実在しない追加講師枠が片瀬の処理で計上され、塚本が該当欄に確認印を付けている。 reveal: - mode: conversation - condition: 片瀬か吉岡に事件前に塚本が確認していた精算書について尋ねたら開示する。 + condition: 片瀬か吉岡に事件前に塚本が確認していた精算書について尋ねたら開示する。または遺体・現場を調べ、「外部講師費の精算書」に関わる資料を確認したら開示する。 sources: - type: character id: katase - type: character id: yoshioka + - type: victim + id: victim supports: [payment-fraud, tsukamoto-found-fraud, tsukamoto-summoned-katase] contradicts: ["lie:katase-no-fake-fee"] - id: copied-material label: 出典のない研修資料 description: 藤堂の資料には他者の文章を出典表示なしで転用した部分があるが、事件のアリバイとは関係しない。 reveal: - mode: conversation condition: 藤堂に研修資料の作成経緯を尋ね、転用を否定したら開示する。 sources: - type: character @@ -377,7 +373,6 @@ evidences: label: 私用交通費の精算 description: 吉岡が私用の交通費を研修経費へ混ぜていたことが分かるが、主事件とは独立している。 reveal: - mode: conversation condition: 吉岡に自分の経費精算を尋ね、私用分を否定したら開示する。 sources: - type: character @@ -388,7 +383,6 @@ evidences: label: 空き客室の私物 description: 間宮が空き客室を私物置き場として使っていたことが分かるが、片瀬の離席とは無関係である。 reveal: - mode: conversation condition: 間宮に空き客室の利用を尋ね、私物置き場を否定したら開示する。 sources: - type: character @@ -400,17 +394,8 @@ solution: summary: 犯人は片瀬真央。架空の講師費を塚本に見抜かれ、書斎へ呼び出された片瀬はゲームの途中で談話室を離れた。間宮がその間だけ青席に座ってゲームを続けたため、青席の得点は途切れず、吉岡が後から清書した得点表にも連続した数字が残った。だが得点表は人物ではなく席を記録するものだった。藤堂は21時25分ごろ片瀬が廊下から戻ってくるところを見ており、間宮も代打を認める。得点表の連続性は片瀬の在席証明ではない。 method: ゲーム中に自分の席を別人へ任せ、席単位で続く得点記録を人物の連続アリバイに見せかけて書斎へ向かった motive: 架空の外部講師費を計上した不正が発覚し、責任追及を恐れたため - requiredFacts: [payment-fraud, tsukamoto-summoned-katase, katase-left-2112, mamiya-substituted-blue, blue-score-continued, katase-killed-tsukamoto, katase-returned-2125, todo-saw-katase-return] secretKeywords: - 犯人は片瀬 - 片瀬が塚本を襲 - 片瀬が青席を離れ - 得点表は片瀬の在席証明ではない -quality: - expectedQuestionCount: - min: 11 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [todo-secret-plagiarism, yoshioka-secret-expense, mamiya-secret-room] - notes: 得点表の連続性を人物の連続性と誤読させる社会的アリバイ。機械ログではなく、記録単位と途中交代の証言を突き合わせて崩す。 diff --git a/db/scenarios/cave-lab-locator-cart.yaml b/db/scenarios/cave-lab-locator-cart.yaml index bdf27e5..700ab96 100644 --- a/db/scenarios/cave-lab-locator-cart.yaml +++ b/db/scenarios/cave-lab-locator-cart.yaml @@ -1,15 +1,33 @@ schemaVersion: 1 id: cave-lab-locator-cart meta: - title: 崩落洞窟、地下研究所の夜 + title: "救助隊が来るまで地下にいる" synopsis: "午後九時二十七分、地下洞窟研究所の地図解析室で、調査主任の岩代圭吾が死亡しているのが見つかりました。午後八時四十分の落盤で唯一の坑道出口が塞がれ、救助隊が到着するまで外部との往来は不可能です。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [洞窟, 落盤, 位置情報, 測量] victim: name: 岩代圭吾 introduction: 地下洞窟研究所調査主任 + foundAt: 21:27 + foundIn: 地図解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 岩代圭吾は地図解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「試料ラベルと測量座標の不一致」に関わる資料が残されている。 +places: + - id: survey-zone + name: 自動測量区画 + shortName: 測量区画 + introduction: 測量カートの校正と位置タグ確認を行う研究区画 + situation: カートと位置タグの充電台が壁沿いに並んでいる + findings: + - id: cart-tag-fastener + statement: 測量カートの収納ベルトには、小型の位置タグを固定できる留め具と新しい擦れ跡がある。 + - id: cart-track-overlay + statement: 端末に残るカートの走行軌跡と位置タグの軌跡は、同じ時間帯に同じ経路を通っている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +59,12 @@ facts: - id: kagawa-forged-sample-origin statement: 香川紗英は成果を大きく見せるため、一部の試料採取地点を実際より深部の区画として記録していた kind: motive - secret: true - id: iwashiro-found-forgery statement: 岩代圭吾は事件当日、香川紗英の測量座標と試料ラベルが一致しないことに気づいた kind: motive - secret: true - id: iwashiro-planned-retraction statement: 岩代圭吾は救助後、香川紗英の成果報告を撤回して調査機関へ訂正を提出する予定だった kind: motive - secret: true - id: locator-tag-removable statement: 安全用位置タグは作業ベストから取り外して別の物へ固定できる kind: physical @@ -59,37 +74,30 @@ facts: - id: kagawa-tag-on-cart statement: 香川紗英は20時58分、位置タグを作業ベストから外して自動測量カートの収納ベルトへ固定した kind: truth - secret: true - id: tag-track-matches-cart statement: 21時00分から21時15分までの香川紗英の位置タグ軌跡は、自動測量カートの走行軌跡と時刻まで一致していた kind: physical - id: kagawa-left-survey-zone statement: 21時01分ごろ、香川紗英は位置タグを残したまま測量区画を離れた kind: truth - secret: true - id: tono-saw-kagawa-2107 statement: 21時07分ごろ、遠野澪は地図解析室へ続く連絡通路で位置タグを付けていない香川紗英を見た kind: observation - id: kagawa-killed-iwashiro statement: 21時11分ごろ、香川紗英は地図解析室で岩代圭吾を襲い死亡させた kind: truth - secret: true - id: kagawa-recovered-tag statement: 21時17分ごろ、香川紗英は測量区画へ戻り、自動カートから位置タグを回収した kind: truth - secret: true - id: shindo-hid-sample statement: 新堂匠は共同管理の試料の一部を自分の研究用ケースへ移していた kind: other - secret: true - id: yuki-bypassed-sensor statement: 結城真は故障警報を避けるため、換気設備の一つのセンサーを手順外で無効化していた kind: other - secret: true - id: tono-edited-time-note statement: 遠野澪は自分の記録漏れを隠すため、夕方の巡回時刻を後から書き直していた kind: other - secret: true - id: body-found-2127 statement: 21時27分、新堂匠が地図解析室で岩代圭吾の死を発見した kind: observation @@ -99,47 +107,57 @@ timeline: at: "20:58" participants: [kagawa] facts: [locator-tag-removable, kagawa-tag-on-cart] + record: 軌跡比較 description: 香川が位置タグを作業ベストから外し、自動測量カートへ固定する。 + location: 研究所内 - id: cart-loop-start at: "21:00" participants: [] facts: [mapping-cart-auto-loop, tag-track-matches-cart] + record: 位置履歴 description: 自動測量カートが無人で校正ルートの周回を始め、香川の位置タグも同じ軌跡を記録する。 + location: 測量区画 - id: kagawa-leaves at: "21:01" participants: [kagawa] facts: [kagawa-left-survey-zone] description: 香川が位置タグをカートに残したまま測量区画を離れる。 + location: 測量区画 - id: tono-sighting at: "21:07" participants: [kagawa, tono] facts: [tono-saw-kagawa-2107] description: 遠野が連絡通路で、位置タグを付けていない香川を目撃する。 + location: 連絡通路 - id: iwashiro-death at: "21:11" participants: [kagawa] facts: [kagawa-killed-iwashiro] description: 香川が地図解析室で岩代を襲い、岩代は死亡する。 + location: 地図解析室 - id: cart-loop-end at: "21:15" participants: [] facts: [mapping-cart-auto-loop, tag-track-matches-cart] + record: 位置履歴 description: 自動測量カートが校正ルートの周回を終える。 + location: 測量区画 - id: tag-recovered at: "21:17" participants: [kagawa] facts: [kagawa-recovered-tag] description: 香川が測量区画へ戻り、自動カートから位置タグを回収する。 + location: 測量区画 - id: discovery at: "21:27" participants: [shindo, kagawa, yuki, tono] facts: [body-found-2127] description: 新堂が地図解析室で岩代の死を発見する。 + location: 地図解析室 characters: - id: kagawa name: 香川紗英 - role: suspect publicIntroduction: "洞窟測量を担当する技術者。" personality: 位置情報と測量値を絶対視する技術者。数字で説明できることには強気だが、成果の正確さを疑われると攻撃的になる。位置タグの連続軌跡を自分の在席証明として繰り返し示す。 goals: @@ -172,7 +190,6 @@ characters: strategy: maintain-until-contradicted memories: - id: retraction-threat - about: iwashiro-planned-retraction detail: 岩代から「救助されたら報告を取り下げ、採取地点の訂正を出す」と言われ、自分の調査成果が全部疑われると思った。 relationships: - character: tono @@ -183,7 +200,6 @@ characters: attitude: 試料の扱いが勝手で信用していない - id: shindo name: 新堂匠 - role: suspect publicIntroduction: "研究成果への競争心が強い地質学者。" personality: 研究成果への競争心が強い地質学者。共同試料を個人ケースへ移したことを隠すため保管区画の話を避けるが、位置タグの仕組みには詳しくない。 goals: @@ -200,7 +216,6 @@ characters: strategy: maintain-until-contradicted memories: - id: cart-loop-sound - about: mapping-cart-auto-loop detail: 21時台、測量区画の自動カートが同じ区間を何度も往復する駆動音を聞いていた。 relationships: - character: kagawa @@ -208,7 +223,6 @@ characters: attitude: 座標の正確さにはうるさい人なので、採取地点の偽りが本当なら意外だと思う - id: yuki name: 結城真 - role: suspect publicIntroduction: "実務優先の設備担当。" personality: 実務優先の設備担当。換気センサーを無効化した手順違反を隠したいが、安全用位置タグの受信設備も保守しており、タグが人そのものを認識しているわけではないと知っている。 goals: @@ -225,12 +239,10 @@ characters: strategy: maintain-until-contradicted memories: - id: tag-is-object - about: locator-tag-removable detail: 安全タグは単純な発信器なので、ベストから外せば人ではなくタグを付けた物の位置を追うだけだと保守上よく知っている。 relationships: [] - id: tono name: 遠野澪 - role: witness publicIntroduction: "調査記録を担当する職員。" personality: 観察した順番を細かく記憶する記録担当。夕方の巡回時刻を書き直したことを隠したいが、21時07分の連絡通路で見た香川の姿は鮮明に覚えている。 goals: @@ -247,7 +259,6 @@ characters: strategy: maintain-until-contradicted memories: - id: kagawa-without-tag - about: tono-saw-kagawa-2107 detail: 21時07分ごろ、胸元にいつもの黄色い位置タグがない香川が解析室側へ急いでいたので、落としたのかと思った。 relationships: [] @@ -309,19 +320,18 @@ evidences: label: 位置タグと自動測量カートの軌跡比較 description: 香川の位置タグと自動測量カートが、21時00分から15分まで同じ地点を同じ時刻に通過している。二つの軌跡は実質的に重なる。 reveal: - mode: conversation - condition: 香川、結城、新堂のいずれかに位置タグの軌跡と自動測量カートの運行を比較できないか尋ねたら開示する。 + condition: 香川、結城、新堂のいずれかに位置タグの軌跡と自動測量カートの運行を比較できないか尋ねたら開示する。または自動測量区画を調べ、カートの走行跡と位置タグの固定跡を照合したら開示する。 sources: - { type: character, id: kagawa } - { type: character, id: yuki } - { type: character, id: shindo } + - { type: location, id: survey-zone } supports: [locator-tag-removable, mapping-cart-auto-loop, kagawa-tag-on-cart, tag-track-matches-cart] contradicts: ["lie:kagawa-location-alibi", "lie:kagawa-wore-tag"] - id: tagless-sighting label: 二十一時七分のタグなし目撃 description: 遠野は21時07分ごろ、位置タグを胸元に付けていない香川を解析室側の連絡通路で見ている。 reveal: - mode: conversation condition: 遠野に21時台の連絡通路で誰を見たか、位置タグの有無も含めて尋ねたら開示する。 sources: - { type: character, id: tono } @@ -331,18 +341,17 @@ evidences: label: 試料ラベルと測量座標の不一致 description: 香川が深部採取として登録した試料のラベルが、実際の測量データでは浅い区画の座標と一致する。岩代の訂正予定も残る。 reveal: - mode: conversation - condition: 香川か新堂に岩代が事件直前に照合していた試料採取地点について尋ね、測量座標との不一致を追及したら開示する。 + condition: 香川か新堂に岩代が事件直前に照合していた試料採取地点について尋ね、測量座標との不一致を追及したら開示する。または遺体・現場を調べ、「試料ラベルと測量座標の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: kagawa } - { type: character, id: shindo } + - { type: victim, id: victim } supports: [kagawa-forged-sample-origin, iwashiro-found-forgery, iwashiro-planned-retraction] contradicts: [] - id: shindo-private-case label: 新堂の個人ケースにある共同試料 description: 共同管理の試料が新堂の個人ケースから見つかるが、解析室の事件とは独立した規約違反である。 reveal: - mode: conversation condition: 新堂に共同試料を個人用に移していないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: shindo } @@ -352,7 +361,6 @@ evidences: label: 結城の換気センサー無効化記録 description: 結城が換気設備の一つのセンサーを手順外で無効化していたことが分かるが、事件とは別件である。 reveal: - mode: conversation condition: 結城に換気センサーを無効化していないか尋ね、手順どおりだったという説明を検証したら開示する。 sources: - { type: character, id: yuki } @@ -362,7 +370,6 @@ evidences: label: 遠野の安全記録修正履歴 description: 夕方の巡回時刻が遠野によって後から書き直されているが、21時07分の目撃とは無関係の記録漏れだった。 reveal: - mode: conversation condition: 遠野に安全記録を後から修正していないか尋ね、編集履歴を検証したら開示する。 sources: - { type: character, id: tono } @@ -374,18 +381,9 @@ solution: summary: 犯人は香川紗英。試料採取地点を実際より深部として記録した偽装を岩代に見抜かれ、救助後に成果報告を撤回される予定だった。香川は20時58分に安全用位置タグを作業ベストから外して自動測量カートへ固定した。カートは21時00分から15分まで無人で校正ルートを周回し、香川のタグはカートと完全に同じ軌跡を記録した。香川本人は21時01分ごろ測量区画を離れ、21時07分には遠野がタグを付けていない香川を解析室側で目撃している。21時11分ごろ岩代を襲い、21時17分ごろ測量区画へ戻ってタグを回収した。位置履歴が示していたのは香川本人ではなく、カートに載せられた発信器だった。 method: 安全用位置タグを無人の自動測量カートへ固定し、その移動軌跡を自分の在席記録に見せかけて解析室へ移動した motive: 試料採取地点の偽装が発覚し、救助後に成果報告を撤回され調査機関へ訂正されることを恐れたため - requiredFacts: [kagawa-forged-sample-origin, iwashiro-planned-retraction, locator-tag-removable, mapping-cart-auto-loop, kagawa-tag-on-cart, tag-track-matches-cart, kagawa-left-survey-zone, tono-saw-kagawa-2107, kagawa-killed-iwashiro] secretKeywords: - 犯人は香川 - 香川が犯人 - 香川が岩代を襲 - 私が岩代を襲 - 位置タグをカートで偽装 -quality: - expectedQuestionCount: - min: 13 - max: 26 - requiredEvidence: - min: 3 - redHerrings: [shindo-hid-sample, yuki-bypassed-sensor, tono-edited-time-note] - notes: 位置情報という機械記録を「タグの場所」と「人の場所」に分ける。軌跡が自動測量カートと時刻まで一致すること、遠野がタグなしの香川本人を目撃していることの二段でアリバイを崩す。 diff --git a/db/scenarios/coldcase-lodge-borrowed-memory.yaml b/db/scenarios/coldcase-lodge-borrowed-memory.yaml index 7914f4d..833fccf 100644 --- a/db/scenarios/coldcase-lodge-borrowed-memory.yaml +++ b/db/scenarios/coldcase-lodge-borrowed-memory.yaml @@ -1,15 +1,31 @@ schemaVersion: 1 id: coldcase-lodge-borrowed-memory meta: - title: 1979年、雪山荘未解決事件 + title: "あの冬、白樺館にいた" synopsis: "2026年。47年前、雪崩で孤立した山荘「白樺館」で起きた未解決事件を再調査します。 1979年2月17日の夜、山荘主・野上修一が書斎で死亡しました。" category: 未解決事件再調査 difficulty: 5 estimatedMinutes: 20 - tags: [1979年, 2026年, コールドケース, 回想] victim: name: 野上修一 introduction: 山荘「白樺館」主人 + foundAt: 23:10 + foundIn: 書斎 + estimatedDeathAt: "22:05" + findings: [] +places: + - id: case-archive + name: 旧捜査資料箱 + shortName: 旧捜査資料 + introduction: 1979年事件の調書・検視記録・現場写真をまとめた保管箱 + situation: 黄ばんだ封筒と写真袋が、作成日順に綴じ直されている + findings: + - id: original-autopsy-time + statement: 当時の検視記録には、死亡は22時05分ごろと見積もられた旨が記されている。 + - id: original-sighting-source + statement: 事件直後の供述調書で22時30分の暖炉前目撃を自分の体験として述べているのは、一人だけである。 + - id: old-expense-ledger + statement: 押収資料の仕入れ帳には、倉田の担当欄の金額を野上が事件当日に再確認した印が残っている。 briefing: |- ——事件の記録を読み上げます。 @@ -53,30 +69,24 @@ facts: - id: maki-stole-cash statement: 高瀬真紀は1979年当時、山荘の現金箱から金を盗んでいた kind: other - secret: true - id: tatsuya-secret-affair statement: 藤村達也は1979年当時、被害者の家族に知られたくない交際関係を隠していた kind: other - secret: true - id: megumi-forged-expenses statement: 倉田恵は山荘の仕入れ代を水増しして差額を取っていた kind: motive - secret: true - id: nogami-found-megumi-fraud statement: 21時50分ごろ、野上修一は倉田恵の仕入れ代水増しに気づき翌朝警察へ相談すると告げた kind: motive - secret: true - id: maki-saw-megumi-study statement: 22時08分ごろ、高瀬真紀は書斎側の廊下から出てくる倉田恵を見た kind: observation - id: megumi-killed-nogami statement: 22時05分ごろ、倉田恵は書斎で野上修一を襲い死亡させた kind: truth - secret: true - id: maki-lied-fireplace statement: 高瀬真紀は自分の現金窃盗の時間を隠すため、22時30分に野上を暖炉前で見たという嘘をついた kind: truth - secret: true - id: body-found-2310 statement: 23時10分、藤村達也が書斎で野上修一の死を発見した kind: observation @@ -86,35 +96,40 @@ timeline: participants: [megumi] facts: [megumi-forged-expenses, nogami-found-megumi-fraud] description: 野上が倉田の仕入れ代水増しを見抜き、翌朝警察へ相談すると告げる。 + location: 山荘内 - id: nogami-death at: "22:05" participants: [megumi] facts: [megumi-killed-nogami] description: 倉田が書斎で野上を襲う。 + location: 書斎 - id: maki-sees-megumi at: "22:08" participants: [maki, megumi] facts: [maki-saw-megumi-study] description: 真紀が書斎側の廊下から出てくる倉田を見かける。 + location: 廊下 - id: megumi-goes-room at: "22:15" participants: [megumi] facts: [megumi-originally-asleep-2215] description: 倉田が自室へ入り、その後は眠っていたと翌朝の供述で話す。 + location: 自室 - id: false-fireplace-sighting at: "22:30" participants: [maki] facts: [maki-lied-fireplace, original-only-maki-claimed-sighting] description: 真紀が自分の窃盗時間を隠すため、野上を暖炉前で見たという架空の目撃を後に申告する。 + location: 暖炉前 - id: discovery at: "23:10" participants: [maki, tatsuya, megumi] facts: [body-found-2310] description: 藤村が書斎で野上の死を発見する。 + location: 書斎 characters: - id: maki name: 高瀬真紀 - role: suspect publicIntroduction: "2026年現在は穏やかな元教師。" personality: 2026年現在は穏やかな元教師。47年間「22時30分に暖炉前で野上を見た」と繰り返してきたため、本人にもそれが固い記憶になっている。だが事件当時の現金窃盗は今でも恥じている。 goals: @@ -137,7 +152,6 @@ characters: strategy: maintain-until-contradicted memories: - id: maki-memory-hardened - about: current-all-remember-2230 detail: 何十年も暖炉前の場面を思い出してきたせいで、新聞で見た写真まで自分の視界だったような感覚がある。 relationships: - character: tatsuya @@ -148,7 +162,6 @@ characters: attitude: 書斎側で見たことだけは今も気になっている - id: tatsuya name: 藤村達也 - role: witness publicIntroduction: "2026年現在は慎重な会社員OB。" personality: 2026年現在は慎重な会社員OB。今は自分も暖炉前の野上を見た気がしているが、古い供述を読むと自信を失う。若いころの秘密の交際だけは話したくない。 goals: @@ -165,8 +178,9 @@ characters: strategy: maintain-until-contradicted memories: - id: tatsuya-source-confusion - about: tatsuya-originally-heard-from-maki detail: 暖炉前の野上の姿を思い浮かべられるが、自分が見たのか真紀の話を何度も聞いたからなのか分からなくなっている。 + - id: death-estimate-memory + detail: 再調査で開封された旧検視記録を読み、当時の死亡推定が22時05分ごろだったことを改めて知っている。 relationships: - character: maki relation: 1979年の友人 @@ -176,7 +190,6 @@ characters: attitude: 事件の夜は早く寝た人だと長年思っていた - id: megumi name: 倉田恵 - role: suspect publicIntroduction: "2026年現在は物静かな元経理職。" personality: 2026年現在は物静かな元経理職。事件当夜は22時15分から眠っていたという最初の供述を強調する。仕入れ代の水増しを問われると防御的になる。 goals: @@ -201,7 +214,6 @@ characters: strategy: maintain-until-contradicted memories: - id: megumi-police-threat - about: nogami-found-megumi-fraud detail: 野上から「朝になったら警察に帳簿を見せる」と言われた瞬間だけは、47年たっても鮮明に覚えている。 relationships: - character: maki @@ -255,7 +267,6 @@ evidences: label: 1979年2月18日の三人の供述調書 description: 真紀だけが22時30分の直接目撃を主張し、藤村は真紀から聞いたと述べ、倉田は22時15分から眠っていたとしている。 reveal: - mode: conversation condition: 誰かに現在の記憶と事件翌朝の供述の違いを尋ね、旧捜査資料を確認したら開示する。 sources: - { type: character, id: maki } @@ -267,7 +278,6 @@ evidences: label: 一週間後の新聞記事 description: 「三人が暖炉前の野上を見た」と誤って要約した記事が、その後何度も事件紹介で引用されている。 reveal: - mode: conversation condition: 藤村か真紀に三人の記憶がいつから一致するようになったのか尋ねたら開示する。 sources: - { type: character, id: maki } @@ -278,7 +288,6 @@ evidences: label: 現金箱の不足メモ description: 事件翌日の帳簿には少額の現金不足があり、真紀の最初の供述では22時台の行動が不自然に曖昧になっている。 reveal: - mode: conversation condition: 真紀に事件当夜の自分の隠し事を追及し、現金不足の旧メモを示したら開示する。 sources: - { type: character, id: maki } @@ -288,7 +297,6 @@ evidences: label: 真紀の最初の館内スケッチ description: 事件翌朝に真紀が描いた簡単な館内図には、22時08分ごろ書斎側で倉田とすれ違った印が残っている。 reveal: - mode: conversation condition: 真紀に暖炉前より前の時間帯で気になった人物を尋ね、旧捜査資料の館内スケッチを確認したら開示する。 sources: - { type: character, id: maki } @@ -298,25 +306,30 @@ evidences: label: 仕入れ帳の水増し description: 倉田の担当欄で仕入れ額が実際より増やされ、野上が事件当日に再確認の印を付けている。 reveal: - mode: conversation - condition: 倉田に野上と事件直前に揉めた帳簿の内容を追及したら開示する。 + condition: 倉田に野上と事件直前に揉めた帳簿の内容を追及したら開示する。または旧捜査資料箱を調べ、押収された仕入れ帳の該当欄を確認したら開示する。 sources: - { type: character, id: megumi } + - { type: location, id: case-archive } supports: [megumi-forged-expenses, nogami-found-megumi-fraud] contradicts: ["lie:megumi-no-fraud"] + - id: death-estimate + label: 1979年の検視記録 + description: 当時の検視記録は、発見時の状態から野上の死亡を22時05分ごろと見積もっている。22時30分の暖炉前目撃より前になる。 + reveal: + condition: 旧捜査資料箱を調べて当時の検視記録を確認するか、藤村に再調査で読み直した検視記録の内容を尋ねたら開示する。 + sources: + - { type: location, id: case-archive } + - { type: character, id: tatsuya } + supports: [megumi-killed-nogami] + contradicts: [] + revealsDeathTime: true solution: culprit: megumi summary: 犯人は倉田恵。1979年、仕入れ代の水増しを野上に見抜かれ、22時05分ごろ書斎で野上を襲った。22時30分に野上が暖炉前で生きていたという長年の定説は、真紀が自分の現金窃盗を隠すために作った虚偽の目撃が源だった。事件後の新聞がそれを「三人の目撃」と誤って報じ、47年間の回想で藤村と倉田の記憶にも混ざった。最初の供述へ戻ると、真紀は22時08分に書斎側から出てくる倉田を見ており、倉田には帳簿不正が発覚する動機もあった。 method: 事件後に別人が作った虚偽の生存目撃が長年の共通記憶へ変化したことを利用し、実際の事件時刻を遅く見せ続けた motive: 仕入れ代の水増しが発覚し、翌朝警察へ相談されるのを防ぐため - requiredFacts: [original-only-maki-claimed-sighting, tatsuya-originally-heard-from-maki, newspaper-published-fireplace-story, maki-lied-fireplace, maki-saw-megumi-study, megumi-forged-expenses, nogami-found-megumi-fraud, megumi-killed-nogami] secretKeywords: - 犯人は倉田 - 倉田が野上を襲 - 真紀の目撃は嘘 - 暖炉前の目撃は嘘 -quality: - expectedQuestionCount: { min: 12, max: 26 } - requiredEvidence: { min: 4 } - redHerrings: [maki-stole-cash, tatsuya-secret-affair] - notes: 現在の証言ではなく事件翌朝の一次資料へ戻るコールドケース型。誤報と反復で記憶が収束した構造を崩した後、当時の目撃と動機へ戻る。 diff --git a/db/scenarios/deepsea-habitat-batch-signature.yaml b/db/scenarios/deepsea-habitat-batch-signature.yaml index e60c0e4..c75e4d5 100644 --- a/db/scenarios/deepsea-habitat-batch-signature.yaml +++ b/db/scenarios/deepsea-habitat-batch-signature.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: deepsea-habitat-batch-signature meta: - title: 深海居住区、海底の四人 + title: "アビス3、浮上不能" synopsis: "午前零時二十二分、海底居住実験区「アビス3」の資料室で、主任研究者の篠宮亮が死亡しているのが見つかりました。海上は暴風で、午後十一時四十分に支援船との昇降カプセル接続が解除されて以降、外部との往来はありません。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [海底基地, 荒天, 点検記録, 時刻] victim: name: 篠宮亮 introduction: 海底居住実験区「アビス3」主任研究者 + foundAt: 00:22 + foundIn: 資料室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 篠宮亮は資料室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「部品交換記録と実機番号の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +48,12 @@ facts: - id: sagisawa-falsified-tests statement: 鷺沢怜は交換を先送りした生命維持部品を正常として点検記録に記載していた kind: motive - secret: true - id: shinomiya-found-falsification statement: 篠宮亮は事件当日、鷺沢怜の点検記録と実際の部品状態が一致しないことに気づいた kind: motive - secret: true - id: shinomiya-planned-report statement: 篠宮亮は帰還後に鷺沢怜を当直から外し、点検記録の改ざんを調査委員会へ提出する予定だった kind: motive - secret: true - id: readings-automatic statement: 生命維持点検画面の00時00分から00時12分の数値はセンサーから二分おきに自動記録された kind: physical @@ -59,37 +63,30 @@ facts: - id: sagisawa-left-control statement: 00時01分ごろ、鷺沢怜は制御室を離れた kind: truth - secret: true - id: kohinata-saw-sagisawa statement: 00時05分ごろ、小日向茜は資料室へ続く中央通路で鷺沢怜とすれ違った kind: observation - id: sagisawa-killed-shinomiya statement: 00時08分ごろ、鷺沢怜は資料室で篠宮亮を襲い死亡させた kind: truth - secret: true - id: sagisawa-returned-control statement: 00時14分ごろ、鷺沢怜は制御室へ戻った kind: truth - secret: true - id: sagisawa-batch-signed statement: 00時16分、鷺沢怜は00時00分から00時12分までの七行を一度に選択して担当者署名を付けた kind: physical - secret: true - id: one-signature-transaction statement: 00時00分から00時12分までの七つの署名は、同じ確認処理番号で00時16分に一括登録された kind: physical - id: narumi-hid-sample statement: 鳴海俊は共同研究規約に反して希少な地質試料を個人研究用に取り置いていた kind: other - secret: true - id: kohinata-deleted-message statement: 小日向茜は自分の通信手順ミスを隠すため、支援船との一件のやり取りを端末上から削除していた kind: other - secret: true - id: toki-bypassed-maintenance statement: 土岐誠は点検期限を過ぎた昇降機構を手順外の応急処置で使い続けていた kind: other - secret: true - id: body-found statement: 00時22分、鳴海俊が資料室で篠宮亮の死を発見した kind: observation @@ -99,41 +96,49 @@ timeline: at: "00:00" participants: [sagisawa] facts: [readings-automatic, signatures-batchable] + record: 点検記録 description: 生命維持点検画面が二分間隔のセンサー値を自動記録し始める。 + location: 制御室 - id: sagisawa-leaves at: "00:01" participants: [sagisawa] facts: [sagisawa-left-control] description: 鷺沢が制御室を離れる。 + location: 制御室 - id: passage-sighting at: "00:05" participants: [sagisawa, kohinata] facts: [kohinata-saw-sagisawa] description: 小日向が中央通路で鷺沢とすれ違う。 + location: 中央通路 - id: shinomiya-death at: "00:08" participants: [sagisawa] facts: [sagisawa-killed-shinomiya] description: 鷺沢が資料室で篠宮を襲い、篠宮は死亡する。 + location: 資料室 - id: sagisawa-return at: "00:14" participants: [sagisawa] facts: [sagisawa-returned-control] description: 鷺沢が制御室へ戻る。 + location: 制御室 - id: batch-sign at: "00:16" participants: [sagisawa] facts: [sagisawa-batch-signed, one-signature-transaction] + record: 署名記録 description: 鷺沢が00時00分から00時12分までの点検行をまとめて確認し、一括で署名を付ける。 + location: 居住区内 - id: discovery at: "00:22" participants: [narumi, sagisawa, kohinata, toki] facts: [body-found] description: 鳴海が資料室で篠宮の死を発見する。 + location: 資料室 characters: - id: sagisawa name: 鷺沢怜 - role: suspect publicIntroduction: "生命維持設備を担当する技術者。" personality: 数字と手順を盾にする生命維持技術者。異常時でも声を荒らげないが、自分の整備判断を否定されると頑なになる。点検画面に署名が並んでいることを強調する。 goals: @@ -166,7 +171,6 @@ characters: strategy: maintain-until-contradicted memories: - id: committee-threat - about: shinomiya-planned-report detail: 篠宮から帰還したら当直を外し、記録を委員会に出すと言われたことが頭から離れない。 relationships: - character: toki @@ -174,7 +178,6 @@ characters: attitude: 現場の応急処置に頼りすぎる点は信用していない - id: narumi name: 鳴海俊 - role: suspect publicIntroduction: "好奇心旺盛で研究成果への執着が強い地質学者。" personality: 好奇心旺盛で研究成果への執着が強い地質学者。規約違反の試料取り置きを知られたくないため試料庫の話を避ける。 goals: @@ -191,12 +194,10 @@ characters: strategy: maintain-until-contradicted memories: - id: batch-screen - about: sagisawa-batch-signed detail: 発見前に制御室の前を通ったとき、鷺沢が点検画面で何行もまとめて選択しているように見えた。 relationships: [] - id: kohinata name: 小日向茜 - role: witness publicIntroduction: "通信担当者。" personality: 時刻と通信記録に几帳面な担当者。自分の手順ミスを消したことには後ろめたさがあるが、中央通路で見た人物と時計表示は正確に覚えている。 goals: @@ -213,12 +214,10 @@ characters: strategy: maintain-until-contradicted memories: - id: saw-sagisawa - about: kohinata-saw-sagisawa detail: 00時05分を示す通信卓の時計を確認した直後、資料室側から来た鷺沢と中央通路ですれ違った。 relationships: [] - id: toki name: 土岐誠 - role: suspect publicIntroduction: "経験則を重んじる機械整備担当。" personality: 経験則を重んじる機械整備担当。期限切れ設備への応急処置を隠すが、点検システムの画面仕様は保守教育で知っている。 goals: @@ -235,7 +234,6 @@ characters: strategy: maintain-until-contradicted memories: - id: batch-training - about: signatures-batchable detail: 忙しい当直向けに複数行をまとめて確認署名できる機能を、以前の保守講習で実演したことがある。 relationships: [] revelations: @@ -282,7 +280,6 @@ evidences: label: 点検画面の確認処理番号 description: 00時00分から00時12分までの七つの署名は同じ処理番号を持ち、00時16分に一度の操作で登録されている。 reveal: - mode: conversation condition: 鷺沢、土岐、小日向のいずれかに点検署名が各時刻に個別入力されたのか尋ねたら開示する。 sources: - { type: character, id: sagisawa } @@ -294,7 +291,6 @@ evidences: label: 零時五分の中央通路目撃 description: 小日向は00時05分ごろ、中央通路で鷺沢とすれ違っている。 reveal: - mode: conversation condition: 小日向に00時ごろの通路で誰と会ったか、時刻の根拠も含めて尋ねたら開示する。 sources: - { type: character, id: kohinata } @@ -304,18 +300,17 @@ evidences: label: 部品交換記録と実機番号の不一致 description: 鷺沢の点検表では交換済みの部品が、実際には古い個体番号のまま残っている。篠宮の調査メモには帰還後の報告予定がある。 reveal: - mode: conversation - condition: 鷺沢か土岐に篠宮が事件直前に照合していた部品番号と点検表について尋ねたら開示する。 + condition: 鷺沢か土岐に篠宮が事件直前に照合していた部品番号と点検表について尋ねたら開示する。または遺体・現場を調べ、「部品交換記録と実機番号の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: sagisawa } - { type: character, id: toki } + - { type: victim, id: victim } supports: [sagisawa-falsified-tests, shinomiya-found-falsification, shinomiya-planned-report] contradicts: [] - id: narumi-sample label: 鳴海が隠した地質試料 description: 共同保管対象の試料が鳴海の個人ケースから見つかるが、資料室の事件とは独立した規約違反である。 reveal: - mode: conversation condition: 鳴海に共同試料を個人用に取り置いていないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: narumi } @@ -325,7 +320,6 @@ evidences: label: 小日向の通信記録削除履歴 description: 小日向の端末から一件の通信記録が削除されているが、事件とは無関係の手順ミスに関するものだった。 reveal: - mode: conversation condition: 小日向に通信記録を消していないか確認し、保存履歴を検証したら開示する。 sources: - { type: character, id: kohinata } @@ -335,7 +329,6 @@ evidences: label: 土岐の期限切れ整備票 description: 土岐が期限切れの昇降機構を応急処置で使っていたことが分かるが、支援船との接続は事件前に解除されている。 reveal: - mode: conversation condition: 土岐に昇降機構の点検期限と応急処置について尋ね、説明を検証したら開示する。 sources: - { type: character, id: toki } @@ -346,18 +339,9 @@ solution: summary: 鷺沢は交換を先送りした生命維持部品を正常として記録していたことを篠宮に見抜かれ、帰還後に当直を外され調査委員会へ提出される予定だった。鷺沢は00時01分ごろ制御室を離れ、00時05分には小日向が中央通路で鷺沢を目撃している。資料室で篠宮を襲い、00時14分ごろ制御室へ戻った後、00時16分に00時00分から12分までの七行へ一括で署名した。各行の数値はセンサーが自動記録したもので、署名も同じ確認処理番号で後からまとめて付けられていた。 method: 自動記録された点検行へ後から一括署名し、それを二分おきに制御室で操作していた証拠に見せかけた motive: 点検記録の改ざんが発覚し、帰還後に当直を外されて調査委員会へ報告されることを恐れたため - requiredFacts: [sagisawa-falsified-tests, shinomiya-planned-report, readings-automatic, signatures-batchable, sagisawa-left-control, kohinata-saw-sagisawa, sagisawa-killed-shinomiya, sagisawa-batch-signed, one-signature-transaction] secretKeywords: - 犯人は鷺沢 - 鷺沢が犯人 - 鷺沢が篠宮を襲 - 私が篠宮を襲 - 一括署名でアリバイを偽装 -quality: - expectedQuestionCount: - min: 13 - max: 26 - requiredEvidence: - min: 3 - redHerrings: [narumi-hid-sample, kohinata-deleted-message, toki-bypassed-maintenance] - notes: 点検表の記録時刻と署名操作時刻を分けることが核心。七行の署名を同一処理番号と一括署名仕様で崩し、小日向の独立した時刻付き目撃を重ねる。 diff --git a/db/scenarios/ferry-fog-passenger-count.yaml b/db/scenarios/ferry-fog-passenger-count.yaml index 32a7437..b169118 100644 --- a/db/scenarios/ferry-fog-passenger-count.yaml +++ b/db/scenarios/ferry-fog-passenger-count.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: ferry-fog-passenger-count meta: - title: 霧のフェリー、航海の途中で - synopsis: "午後五時二十分、離島航路のフェリー「しおかぜ」の上部ラウンジで、運航会社の監査役・柴田功が死亡しているのが見つかりました。船は濃霧のため減速して航行中で、出航後に乗り降りした者はいません。" + title: "霧航船しおかぜ号の謎" + synopsis: "午後五時二十分、離島航路のフェリー「しおかぜ」の上部ラウンジで、運航会社の監査役・柴田功が死亡しているのが見つかりました。船は濃霧のため\ + 減速して航行中で、出航後に乗り降りした者はいません。" category: 船上ミステリ difficulty: 3 estimatedMinutes: 10 - tags: [フェリー, 濃霧, 点呼表] victim: name: 柴田功 introduction: 離島航路運航会社監査役 + foundAt: 17:20 + foundIn: 上部ラウンジ + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 柴田功は上部ラウンジで倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「追加券の控えと売上帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -44,41 +52,33 @@ facts: - id: kanda-skimmed-sales statement: 神田美奈は船内で現金販売した追加券の一部を売上から抜き、帳簿を少なく記録していた kind: motive - secret: true - id: shibata-found-shortage statement: 柴田功は事件当日、追加券の控えと帳簿を照合し、神田美奈による売上金の抜き取りを把握した kind: motive - secret: true - id: shibata-warned-kanda statement: 16時48分ごろ、柴田功は神田美奈に、到着後すぐ本社へ不正を報告すると告げた kind: motive - secret: true - id: fujiwara-secret-interview statement: 16時50分から16時56分まで、藤原奈緒は上部ラウンジで柴田功から会社の安全管理に関する内部資料を見せてもらった kind: observation - secret: true - id: kanda-wrote-count-sheet statement: 16時58分ごろ、神田美奈は予約名簿の42人という数字を写して点呼表を作成した kind: truth - secret: true - id: ogiwara-saw-kanda-1703 statement: 17時03分ごろ、荻原陸は上部ラウンジへ向かう階段で神田美奈とすれ違った kind: observation - id: kanda-killed-shibata-1706 statement: 17時06分ごろ、神田美奈は上部ラウンジで柴田功を襲い死亡させた kind: truth - secret: true - id: kanda-returned-lower-1711 statement: 17時11分ごろ、神田美奈は下部客室へ戻った kind: truth - secret: true - id: count-sheet-says-42 statement: 神田美奈の点呼表には17時12分の記入時刻と乗客42人という人数が書かれていた kind: physical - id: ogiwara-let-friend-bridge statement: 荻原陸は出航前、知人の乗客を短時間だけ乗員通路へ入れて操舵室前から写真を撮らせていた kind: other - secret: true - id: body-found-1720 statement: 17時20分、荻原陸が上部ラウンジで柴田功の死を発見した kind: observation @@ -86,53 +86,68 @@ timeline: - id: passenger-leaves at: "16:35" participants: [] - facts: [passenger-left-before-departure, booked-passengers-42] + facts: [ passenger-left-before-departure, booked-passengers-42 ] + record: 乗降記録 description: 予約客の一人が出航前に下船し、実際の乗客は41人になる。 + location: 船内 - id: shibata-warning at: "16:48" - participants: [kanda] - facts: [kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda] + participants: [ kanda ] + facts: [ kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda ] description: 柴田が神田の売上金抜き取りを指摘し、到着後の本社報告を告げる。 + location: 船内 - id: secret-interview at: "16:50" - participants: [fujiwara] - facts: [fujiwara-secret-interview] + participants: [ fujiwara ] + facts: [ fujiwara-secret-interview ] description: 藤原が上部ラウンジで柴田から内部資料を見せてもらう。 + location: 上部ラウンジ - id: count-sheet-made at: "16:58" - participants: [kanda] - facts: [kanda-wrote-count-sheet] + participants: [ kanda ] + facts: [ kanda-wrote-count-sheet ] description: 神田が実際の点呼をせず、予約名簿の42人という数字を点呼表へ写す。 + location: 船内 - id: stair-sighting at: "17:03" - participants: [kanda, ogiwara] - facts: [ogiwara-saw-kanda-1703] + participants: [ kanda, ogiwara ] + facts: [ ogiwara-saw-kanda-1703 ] description: 荻原が上部ラウンジへ向かう階段で神田とすれ違う。 + location: 階段 - id: shibata-death at: "17:06" - participants: [kanda] - facts: [kanda-killed-shibata-1706] + participants: [ kanda ] + facts: [ kanda-killed-shibata-1706 ] description: 神田が上部ラウンジで柴田を襲い、柴田は死亡する。 + location: 上部ラウンジ - id: kanda-returns at: "17:11" - participants: [kanda] - facts: [kanda-returned-lower-1711, count-sheet-says-42] + participants: [ kanda ] + facts: [ kanda-returned-lower-1711, count-sheet-says-42 ] + record: 点呼表 description: 神田が下部客室へ戻り、17時12分の時刻を点呼表へ記入する。 + location: 下部客室 - id: discovery at: "17:20" - participants: [ogiwara, kanda, fujiwara] - facts: [body-found-1720] + participants: [ ogiwara, kanda, fujiwara ] + facts: [ body-found-1720 ] description: 荻原が上部ラウンジで柴田の死を発見する。 + location: 上部ラウンジ characters: - id: kanda name: 神田美奈 - role: suspect publicIntroduction: "手際がよく、乗客対応にも慣れた事務長。" personality: 手際がよく、乗客対応にも慣れた事務長。数字と帳簿を扱う仕事に自信を持ち、記録さえ整っていれば説明は通ると考えがち。柴田の監査には以前から強い緊張を感じていた。 goals: - 船内売上金の抜き取りを隠したい - 16時55分から17時15分まで下部客室で点呼していたと思わせたい - knowledge: [kanda-is-purser, booked-passengers-42, count-sheet-says-42, body-found-1720] + knowledge: + [ + kanda-is-purser, + booked-passengers-42, + count-sheet-says-42, + body-found-1720 + ] secrets: - fact: kanda-skimmed-sales disclosure: pressured @@ -153,7 +168,6 @@ characters: strategy: maintain-until-contradicted memories: - id: audit-warning - about: shibata-warned-kanda detail: 柴田に追加券の控えを並べられ「港に着いたら本社へ電話する」と言われたとき、逃げ道がなくなったと思った。 relationships: - character: ogiwara @@ -161,13 +175,12 @@ characters: attitude: 規則に甘いところがあると見ている - id: ogiwara name: 荻原陸 - role: witness publicIntroduction: "明るく人懐っこい若い甲板員。" personality: 明るく人懐っこい若い甲板員。乗客へのサービス精神が強すぎて、出航前に知人を乗員通路へ入れた規則違反を隠したい。船内で見た人の移動はよく覚えている。 goals: - 知人を乗員通路へ入れたことを隠したい - 17時03分に神田とすれ違った事実は伝えたい - knowledge: [ogiwara-is-deckhand, ogiwara-saw-kanda-1703, body-found-1720] + knowledge: [ ogiwara-is-deckhand, ogiwara-saw-kanda-1703, body-found-1720 ] secrets: - fact: ogiwara-let-friend-bridge disclosure: pressured @@ -178,7 +191,6 @@ characters: strategy: maintain-until-contradicted memories: - id: stair-meeting - about: ogiwara-saw-kanda-1703 detail: 17時03分ごろ、神田が下から上へ急いで階段を上がってきた。点呼中だと思っていたので少し意外だった。 relationships: - character: kanda @@ -186,13 +198,12 @@ characters: attitude: 厳しいが仕事は正確な人だと思っていた - id: fujiwara name: 藤原奈緒 - role: suspect publicIntroduction: "粘り強い地方紙記者。" personality: 粘り強い地方紙記者。情報源を守る意識が強く、柴田との秘密の面会を簡単には認めない。会社の安全管理問題を追っていたため、監査役との接触自体が取材先へ漏れることを恐れている。 goals: - 柴田から内部資料を受け取ったことを隠したい - 取材源を守りたい - knowledge: [fujiwara-is-reporter, passenger-left-before-departure, body-found-1720] + knowledge: [ fujiwara-is-reporter, passenger-left-before-departure, body-found-1720 ] secrets: - fact: fujiwara-secret-interview disclosure: pressured @@ -203,7 +214,6 @@ characters: strategy: maintain-until-contradicted memories: - id: internal-documents - about: fujiwara-secret-interview detail: 柴田が資料を渡しながら「会社は数字を綺麗に見せすぎている」と小声で言ったことを覚えている。 relationships: [] revelations: @@ -220,14 +230,20 @@ revelations: revealCondition: 藤原に出航前に下船した乗客について確認し、点呼表の42人と実乗船41人の差を指摘した。 requires: revelations: [] - evidences: [boarding-record] + evidences: [ boarding-record ] - type: character id: kanda revealCondition: 神田に42人をどのように数えたか問い、出航前に一人下船した記録との矛盾を示した。 requires: revelations: [] - evidences: [boarding-record] - relatedFacts: [booked-passengers-42, passenger-left-before-departure, kanda-wrote-count-sheet, count-sheet-says-42] + evidences: [ boarding-record ] + relatedFacts: + [ + booked-passengers-42, + passenger-left-before-departure, + kanda-wrote-count-sheet, + count-sheet-says-42 + ] - id: cash-shortage-motive title: 監査で見つかった売上金の不足 text: 柴田は追加券の控えと帳簿の差から、神田が現金売上の一部を抜いていたことを把握し、到着後の本社報告を告げていた。 @@ -240,15 +256,14 @@ revelations: id: kanda revealCondition: 神田に追加券の売上差額と柴田から到着後に何をすると告げられたかを追及し、不正発覚への恐れが明確になった。 requires: - revelations: [forty-second-passenger] - evidences: [ticket-ledger] - relatedFacts: [kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda] + revelations: [ forty-second-passenger ] + evidences: [ ticket-ledger ] + relatedFacts: [ kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda ] evidences: - id: boarding-record label: 出航前の乗降記録 description: 予約名簿は42人だが、16時35分に一人が下船しており、実際の乗客は41人だった。 reveal: - mode: conversation condition: 神田に点呼人数の根拠を尋ねるか、藤原か荻原に出航直前に下船した人がいなかったか確認したら開示する。 sources: - type: character @@ -257,71 +272,60 @@ evidences: id: ogiwara - type: character id: fujiwara - supports: [booked-passengers-42, passenger-left-before-departure] - contradicts: ["lie:kanda-passenger-count-alibi"] + supports: [ booked-passengers-42, passenger-left-before-departure ] + contradicts: [ "lie:kanda-passenger-count-alibi" ] - id: stair-sighting label: 十七時三分の階段の目撃 description: 荻原は17時03分ごろ、下部客室ではなく上部ラウンジへ向かう神田とすれ違っている。 reveal: - mode: conversation condition: 荻原に17時前後の巡回で誰とどこですれ違ったか尋ねたら開示する。 sources: - type: character id: ogiwara - supports: [ogiwara-saw-kanda-1703] - contradicts: ["lie:kanda-passenger-count-alibi"] + supports: [ ogiwara-saw-kanda-1703 ] + contradicts: [ "lie:kanda-passenger-count-alibi" ] - id: ticket-ledger label: 追加券の控えと売上帳簿 description: 現金で販売された追加券の枚数に対し、帳簿へ記録された売上が継続的に少ない。 reveal: - mode: conversation - condition: 神田に柴田が監査していた帳簿の内容を尋ねるか、藤原に会社の数字で柴田が問題視していた点を尋ねたら開示する。 + condition: 神田に柴田が監査していた帳簿の内容を尋ねるか、藤原に会社の数字で柴田が問題視していた点を尋ねたら開示する。または遺体・現場を調べ、「追加券の控えと売上帳簿」に関わる資料を確認したら開示する。 sources: - type: character id: kanda - type: character id: fujiwara - supports: [kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda] + - type: victim + id: victim + supports: [ kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda ] contradicts: [] - id: interview-notes label: 藤原の取材メモ description: 16時50分から柴田と会い、安全管理の内部資料を受け取った記録があるが、16時56分には面会を終えている。 reveal: - mode: conversation condition: 藤原に柴田と当日接触していないという説明を問い直し、取材メモの時刻を確認したら開示する。 sources: - type: character id: fujiwara - supports: [fujiwara-secret-interview] - contradicts: ["lie:fujiwara-no-meeting"] + supports: [ fujiwara-secret-interview ] + contradicts: [ "lie:fujiwara-no-meeting" ] - id: crew-corridor-photo label: 乗員通路から撮られた出航前の写真 description: 荻原の知人が乗員専用区域から撮った写真があり、荻原の規則違反は分かるが事件時刻とは無関係である。 reveal: - mode: conversation condition: 荻原に出航前の乗員通路への立ち入りについて尋ね、誰も入れていないと否定したら開示する。 sources: - type: character id: ogiwara - supports: [ogiwara-let-friend-bridge] - contradicts: ["lie:ogiwara-no-guest-access"] + supports: [ ogiwara-let-friend-bridge ] + contradicts: [ "lie:ogiwara-no-guest-access" ] solution: culprit: kanda summary: 犯人は神田美奈。船内追加券の現金売上を抜いていたことを柴田に見つけられ、到着後すぐ本社へ報告すると告げられた。神田は16時58分ごろ、実際に客室を回る代わりに予約名簿の42人という数字を点呼表へ写し、点呼中というアリバイを準備した。しかし予約客の一人は16時35分に下船しており、実乗船者は41人だった。実際に数えたなら42人にはならない。さらに17時03分、荻原は上部ラウンジへ向かう神田を目撃している。神田は17時06分ごろ柴田を襲い、17時11分ごろ下部客室へ戻って点呼表へ時刻を書き込んだ。藤原の秘密取材と荻原の規則違反は独立したミスリードである。 method: 予約名簿の人数を点呼表へ写して下部客室にいたように装い、その時間に上部ラウンジへ移動して柴田を襲った motive: 売上金の抜き取りを監査で見抜かれ、到着後に本社へ報告されることを恐れたため - requiredFacts: [passenger-left-before-departure, kanda-skimmed-sales, shibata-warned-kanda, kanda-wrote-count-sheet, ogiwara-saw-kanda-1703, kanda-killed-shibata-1706, count-sheet-says-42] secretKeywords: - 犯人は神田 - 神田が犯人 - 神田が柴田を襲 - 私が柴田を襲 - 点呼表を偽造して殺 -quality: - expectedQuestionCount: - min: 8 - max: 18 - requiredEvidence: - min: 2 - redHerrings: [fujiwara-secret-interview, ogiwara-let-friend-bridge] - notes: 核心は「人数が合っているから点呼した」という前提を逆転させること。実人数41人と記録42人の差で点呼表そのものを崩し、荻原の階段目撃を第二の独立経路にする。 diff --git a/db/scenarios/festival-lantern-blackout.yaml b/db/scenarios/festival-lantern-blackout.yaml index 4e6765d..15a6e1b 100644 --- a/db/scenarios/festival-lantern-blackout.yaml +++ b/db/scenarios/festival-lantern-blackout.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: festival-lantern-blackout meta: - title: 夏祭り、祭具倉庫の夜 + title: "八坂野の灯が消えるころ" synopsis: "午後八時三十五分、八坂野神社の夏祭りで、実行委員長の神谷宗一が祭具倉庫の中で死亡しているのが見つかりました。会場には多くの客がいましたが、事件の直前、午後八時十八分から八時二十六分まで提灯と舞台照明が一斉に消えるトラブルが起きています。" category: 祭りの夜 difficulty: 3 estimatedMinutes: 10 - tags: [夏祭り, 停電, アリバイ] victim: name: 神谷宗一 introduction: 八坂野神社夏祭り実行委員長 + foundAt: 20:35 + foundIn: 祭具倉庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 神谷宗一は祭具倉庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「照明設備の発注帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,41 +45,33 @@ facts: - id: kamiya-found-kickbacks statement: 神谷宗一は照明設備の発注額が水増しされ、戸塚岳へ不正な謝礼が流れていることに気づいていた kind: motive - secret: true - id: kamiya-warned-tozuka statement: 事件当日の19時50分ごろ、神谷宗一は戸塚岳に帳簿を見せ、祭り終了後に発注不正を公表すると告げた kind: motive - secret: true - id: makabe-used-donations statement: 真壁聡は神社の修繕費を補うため、祭りの寄付金から一時的に金を流用していた kind: motive - secret: true - id: aihara-unlicensed-stall statement: 相原夏帆は知人の屋台一軒を正式な申請前に出店させており、神谷宗一から注意を受けていた kind: other - secret: true - id: blackout-started-2018 statement: 20時18分、会場の提灯と舞台照明が一斉に消えた kind: physical - id: lighting-preset-ran statement: 停電中の20時18分から20時26分まで、照明制御盤は事前に設定された自動復旧シーケンスを実行していた kind: physical - secret: true - id: tozuka-left-console-2020 statement: 20時20分ごろ、戸塚岳は照明操作卓を離れ、祭具倉庫へ向かった kind: truth - secret: true - id: aihara-saw-tozuka-2022 statement: 20時22分ごろ、相原夏帆は非常灯の下で祭具倉庫の方向へ急ぐ戸塚岳を見た kind: observation - id: tozuka-killed-kamiya-2023 statement: 20時23分ごろ、戸塚岳は祭具倉庫内で神谷宗一を襲い死亡させた kind: truth - secret: true - id: tozuka-returned-2025 statement: 20時25分ごろ、戸塚岳は照明操作卓へ戻った kind: truth - secret: true - id: lights-restored-2026 statement: 20時26分、自動復旧シーケンスが完了して提灯と舞台照明が再点灯した kind: physical @@ -85,57 +84,66 @@ facts: - id: spare-key-borrowed statement: 戸塚岳は祭り準備中に配線確認を理由として祭具倉庫の予備鍵を借りた経験があり、鍵箱の暗証番号を知っていた kind: truth - secret: true timeline: - id: kamiya-warning at: "19:50" participants: [tozuka] facts: [kamiya-warned-tozuka, kamiya-found-kickbacks] description: 神谷が戸塚に発注帳簿を示し、祭り終了後に不正を公表すると告げる。 + location: 事務室 - id: blackout-start at: "20:18" participants: [tozuka, makabe, aihara] facts: [blackout-started-2018, lighting-preset-ran] + record: 制御盤ログ description: 会場の照明が一斉に消え、自動復旧シーケンスが始まる。 + location: 会場 - id: tozuka-leaves at: "20:20" participants: [tozuka] facts: [tozuka-left-console-2020] description: 戸塚が操作卓を離れ、暗い境内を祭具倉庫へ向かう。 + location: 祭具倉庫 - id: aihara-sighting at: "20:22" participants: [tozuka, aihara] facts: [aihara-saw-tozuka-2022] description: 相原が非常灯の下で祭具倉庫方向へ急ぐ戸塚を目撃する。 + location: 倉庫前 - id: kamiya-death at: "20:23" participants: [tozuka] facts: [tozuka-killed-kamiya-2023, spare-key-borrowed] description: 戸塚が祭具倉庫へ入り、神谷を襲う。 + location: 祭具倉庫 - id: tozuka-returns at: "20:25" participants: [tozuka] facts: [tozuka-returned-2025] description: 戸塚が照明操作卓へ戻る。 + location: 操作卓 - id: lights-return at: "20:26" participants: [tozuka, makabe, aihara] facts: [lights-restored-2026] + record: 復旧ログ description: 自動復旧シーケンスが完了し、会場の照明が戻る。 + location: 会場 - id: key-check at: "20:28" participants: [makabe] facts: [makabe-checked-key-2028] description: 真壁が予備鍵の箱を確認し、鍵が戻されているのを見る。 + location: 境内 - id: discovery at: "20:35" participants: [makabe, aihara, tozuka] facts: [body-found-2035] description: 真壁が祭具倉庫で神谷の死を発見する。 + location: 祭具倉庫 characters: - id: tozuka name: 戸塚岳 - role: suspect publicIntroduction: "現場慣れした電気技師で、トラブル時ほど落ち着いて話す。" personality: 現場慣れした電気技師で、トラブル時ほど落ち着いて話す。専門知識への自負が強く、素人には設備の細部まで分からないと思っている。神谷とは発注費を巡って関係が悪化していた。 goals: @@ -162,7 +170,6 @@ characters: strategy: maintain-until-contradicted memories: - id: ledger-threat - about: kamiya-warned-tozuka detail: 神谷が帳簿のコピーを机に置き「今夜で終わりにする」と言ったとき、祭りの音が急に遠くなった気がした。 relationships: - character: aihara @@ -170,7 +177,6 @@ characters: attitude: 現場の事情を知らないのに口を出す人だと思っている - id: makabe name: 真壁聡 - role: suspect publicIntroduction: "真面目で融通の利かない禰宜。" personality: 真面目で融通の利かない禰宜。神社を守る責任感は強いが、修繕費の不足から寄付金を一時流用したことを深く後悔している。鍵の管理について聞かれると必要以上に神経質になる。 goals: @@ -189,12 +195,10 @@ characters: strategy: maintain-until-contradicted memories: - id: returned-key - about: makabe-checked-key-2028 detail: 停電が戻った後、心配になって鍵箱を確認し、予備鍵がいつもの位置に戻っていたので少し安心した。 relationships: [] - id: aihara name: 相原夏帆 - role: witness publicIntroduction: "活発で世話焼きな屋台会のまとめ役。" personality: 活発で世話焼きな屋台会のまとめ役。祭りの成功を優先するあまり、申請前の知人を出店させた後ろめたさがある。暗闇で見た人物について断定するのをためらうが、服装と歩き方には自信がある。 goals: @@ -211,7 +215,6 @@ characters: strategy: evasive memories: - id: reflective-vest - about: aihara-saw-tozuka-2022 detail: 非常灯の下を横切った反射ベストが一瞬だけ光り、戸塚の早足の歩き方だと思ったことを覚えている。 relationships: - character: tozuka @@ -253,7 +256,6 @@ evidences: label: 照明制御盤の自動復旧ログ description: 20時18分から20時26分まで、制御盤が登録済みの復旧シーケンスを自動実行していた記録が残る。 reveal: - mode: conversation condition: 戸塚に停電中の操作内容を尋ねるか、相原に復旧作業で操作卓へ人が張り付く必要があったのか確認したら開示する。 sources: - type: character @@ -266,7 +268,6 @@ evidences: label: 非常灯の下の反射ベスト description: 20時22分ごろ、相原は祭具倉庫方向へ急ぐ戸塚を非常灯の下で見ている。 reveal: - mode: conversation condition: 相原に停電中に見た人影や反射ベストについて具体的に尋ね、戸塚だと思った理由まで話したら開示する。 sources: - type: character @@ -277,7 +278,6 @@ evidences: label: 祭具倉庫の予備鍵の貸出記録 description: 戸塚は準備期間中に予備鍵を借りており、その際に鍵箱の暗証番号を知る機会があった。 reveal: - mode: conversation condition: 真壁に予備鍵を過去に誰へ貸したか尋ねるか、戸塚に倉庫へ入った経験があるか確認したら開示する。 sources: - type: character @@ -290,20 +290,20 @@ evidences: label: 照明設備の発注帳簿 description: 同規模の設備と比べて発注額が不自然に高く、戸塚へ還流した謝礼を神谷が照合したメモが挟まれている。 reveal: - mode: conversation - condition: 戸塚に設備費の内訳を尋ねるか、真壁に神谷が祭り前から確認していた帳簿について尋ねたら開示する。 + condition: 戸塚に設備費の内訳を尋ねるか、真壁に神谷が祭り前から確認していた帳簿について尋ねたら開示する。または遺体・現場を調べ、「照明設備の発注帳簿」に関わる資料を確認したら開示する。 sources: - type: character id: tozuka - type: character id: makabe + - type: victim + id: victim supports: [kamiya-found-kickbacks, kamiya-warned-tozuka] contradicts: [] - id: donation-shortfall label: 神社の寄付金収支の不足 description: 真壁が修繕費へ一時流用した金額が確認できるが、神谷の死亡時刻や停電とは関係がない。 reveal: - mode: conversation condition: 真壁に神谷との金銭上の揉め事がなかったか尋ね、寄付金の扱いを強く否定したら開示する。 sources: - type: character @@ -315,18 +315,9 @@ solution: summary: 犯人は戸塚岳。照明設備の発注水増しと謝礼の受領を神谷に知られ、祭り終了後に公表されると告げられていた。20時18分に照明トラブルが起きると、実際には制御盤が自動復旧を始めていたにもかかわらず「操作卓で手動復旧していた」とアリバイを作り、20時20分ごろその場を離れた。20時22分には相原が祭具倉庫方向へ急ぐ戸塚を見ており、戸塚は予備鍵の暗証番号も知っていた。20時23分ごろ倉庫内で神谷を襲い、20時25分に操作卓へ戻る。20時26分に照明が自動で復旧したため、外からは戸塚がずっと対応していたように見えた。真壁の寄付金流用と相原の出店問題は独立した隠し事である。 method: 停電中の自動復旧を利用して操作卓を離れ、祭具倉庫で神谷を襲った後に操作卓へ戻った motive: 発注不正と謝礼の受領が祭り終了後に公表されることを恐れたため - requiredFacts: [kamiya-found-kickbacks, kamiya-warned-tozuka, lighting-preset-ran, tozuka-left-console-2020, aihara-saw-tozuka-2022, tozuka-killed-kamiya-2023, spare-key-borrowed] secretKeywords: - 犯人は戸塚 - 戸塚が犯人 - 戸塚が神谷を襲 - 私が神谷を襲 - 停電をアリバイに殺 -quality: - expectedQuestionCount: - min: 9 - max: 19 - requiredEvidence: - min: 2 - redHerrings: [makabe-used-donations, aihara-unlicensed-stall] - notes: 核心は「停電復旧はその場に人が必要」という思い込みを制御ログで外し、相原の目撃と戸塚の操作卓アリバイを衝突させること。鍵の入手可能性は補強であり、単独で犯人を決める証拠にはしない。 diff --git a/db/scenarios/flood-archive-self-locking-vault.yaml b/db/scenarios/flood-archive-self-locking-vault.yaml index 970b19b..c3b5a1e 100644 --- a/db/scenarios/flood-archive-self-locking-vault.yaml +++ b/db/scenarios/flood-archive-self-locking-vault.yaml @@ -1,15 +1,33 @@ schemaVersion: 1 id: flood-archive-self-locking-vault meta: - title: 高潮の文書館、閉館後の夜 + title: "水は紙より先に来る" synopsis: "午後九時四十分、海辺歴史文書館の希少資料庫で、館長の今泉孝臣が死亡しているのが見つかりました。高潮で防潮扉が閉じ、午後八時半以降に館外へ出入りした者はいません。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [文書館, 高潮, 密室, 自動施錠] victim: name: 今泉孝臣 introduction: 海辺歴史文書館館長 + foundAt: 21:40 + foundIn: 資料庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 今泉孝臣は資料庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「修復処置記録と資料状態の不一致」に関わる資料が残されている。 +places: + - id: rare-vault + name: 希少資料庫 + shortName: 資料庫 + introduction: 貴重資料を保管する、厚い防火扉の資料庫 + situation: 重い扉が閉じ、廊下側には開錠用の鍵穴がある + findings: + - id: self-locking-latch + statement: 扉は廊下側から開けるときだけ鍵を使い、外へ出て閉じればラッチが自動で掛かる構造になっている。 + - id: door-contact-window + statement: 扉センサーには21時09分から21時22分まで開放が続き、その後に閉じた記録が残っている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +59,12 @@ facts: - id: yagami-forged-restoration-record statement: 八神琴子は修復失敗を隠すため、一部の処置記録と作業日を改ざんしていた kind: motive - secret: true - id: imaizumi-found-forgery statement: 今泉孝臣は事件当日、八神琴子の処置記録と資料の実状態が一致しないことを発見した kind: motive - secret: true - id: imaizumi-planned-review statement: 今泉孝臣は翌朝、八神琴子を重要資料の修復から外し、外部委員へ処置記録を提出する予定だった kind: motive - secret: true - id: vault-key-only-opens statement: 希少資料庫の鍵は廊下側から扉を開けるために必要だが、扉を閉めて施錠するためには必要ない kind: physical @@ -65,7 +80,6 @@ facts: - id: yagami-entered-vault-2114 statement: 21時14分ごろ、八神琴子は開いたままの希少資料庫へ入った kind: truth - secret: true - id: kuga-saw-yagami-2114 statement: 21時14分ごろ、久我遼は希少資料庫へ入る八神琴子を見た kind: observation @@ -75,11 +89,9 @@ facts: - id: yagami-killed-imaizumi statement: 21時20分ごろ、八神琴子は希少資料庫で今泉孝臣を襲い死亡させた kind: truth - secret: true - id: yagami-left-vault-2122 statement: 21時22分ごろ、八神琴子は希少資料庫から廊下へ出て扉を閉めた kind: truth - secret: true - id: vault-closed-2122 statement: 21時22分、希少資料庫の扉が閉じ、自動施錠された kind: physical @@ -89,15 +101,12 @@ facts: - id: kuga-tore-document statement: 久我遼は整理中に資料の一部を傷めたことを報告せず、修復待ち箱へ紛れ込ませていた kind: other - secret: true - id: tozuka-disabled-camera statement: 戸塚誠は私用の読書を隠すため、閲覧室の監視カメラを一時的に停止していた kind: other - secret: true - id: majima-hid-photo-swap statement: 真島香苗は鑑定の誤りを隠すため、提出予定の来歴写真を別の撮影データへ差し替えていた kind: other - secret: true - id: body-found-2140 statement: 21時40分、戸塚誠が予備開錠手順で希少資料庫を開け、今泉孝臣の死を発見した kind: observation @@ -107,37 +116,52 @@ timeline: at: "21:09" participants: [] facts: [vault-key-only-opens, vault-self-locks, vault-open-2109, vault-stayed-open] + record: 扉記録 description: 今泉が鍵で希少資料庫を開け、資料搬入のため扉を開いたままにする。 + location: 希少資料庫 - id: yagami-enters at: "21:14" - participants: [yagami, kuga] + participants: [yagami] + witnesses: [kuga] facts: [yagami-entered-vault-2114, kuga-saw-yagami-2114] description: 八神が開いたままの資料庫へ入り、久我がその姿を目撃する。 + location: 資料庫 + - id: kuga-vault-front + at: "21:14" + participants: [kuga] + facts: [kuga-saw-yagami-2114] + description: 久我が資料庫前から、八神が開放中の資料庫へ入るのを目撃する。 + location: 資料庫前 - id: imaizumi-enters at: "21:17" participants: [] facts: [imaizumi-entered-vault-2117] description: 今泉が資料箱を持って資料庫へ入り、開錠鍵をポケットへ戻す。 + location: 資料庫 - id: imaizumi-death at: "21:20" participants: [yagami] facts: [yagami-killed-imaizumi] description: 八神が希少資料庫で今泉を襲い、今泉は死亡する。 + location: 希少資料庫 - id: yagami-exits at: "21:22" participants: [yagami] facts: [yagami-left-vault-2122, vault-closed-2122] + record: 扉記録 description: 八神が資料庫から出て扉を閉め、自動施錠させる。 + location: 資料庫 - id: discovery at: "21:40" participants: [tozuka, yagami, kuga, majima] facts: [key-found-on-imaizumi, body-found-2140] + record: 予備開錠記録 description: 戸塚が予備開錠手順で資料庫を開け、今泉の死とポケットの鍵を確認する。 + location: 資料庫 characters: - id: yagami name: 八神琴子 - role: suspect publicIntroduction: "手仕事への誇りが高く、修復結果を批判されることに強く反発する修復士。" personality: 手仕事への誇りが高く、修復結果を批判されることに強く反発する修復士。鍵が今泉のポケットにあったことを何度も強調し、資料庫へ入れるはずがないという印象を作ろうとする。 goals: @@ -169,7 +193,6 @@ characters: strategy: maintain-until-contradicted memories: - id: review-threat - about: imaizumi-planned-review detail: 今泉から「明朝から重要資料には触れさせない。外部委員にも記録を出す」と言われ、自分の技術そのものを否定されたように感じた。 relationships: - character: kuga @@ -180,7 +203,6 @@ characters: attitude: 修復の判断へ口を出してくるので苦手 - id: kuga name: 久我遼 - role: witness publicIntroduction: "細かな動線を覚えている学芸員。" personality: 細かな動線を覚えている学芸員。資料を傷めたことを隠したいが、21時14分ごろ八神が開いた資料庫へ入った場面ははっきり見ている。 goals: @@ -197,12 +219,10 @@ characters: strategy: maintain-until-contradicted memories: - id: yagami-open-door - about: kuga-saw-yagami-2114 detail: 21時14分ごろ、資料庫の扉が搬入で開きっぱなしになっていて、八神が鍵を使わずそのまま中へ入るのを見た。 relationships: [] - id: tozuka name: 戸塚誠 - role: witness publicIntroduction: "文書館の警備担当。" personality: 規則には詳しい警備担当。閲覧室のカメラ停止を隠したいが、希少資料庫の扉が閉まるだけで自動施錠される仕様は日常業務として知っている。 goals: @@ -219,12 +239,10 @@ characters: strategy: maintain-until-contradicted memories: - id: self-locking-door - about: vault-self-locks detail: 希少資料庫は開ける時だけ通常鍵が必要で、出た人が扉を引けばラッチが掛かって自動施錠される。鍵を差して閉める操作はない。 relationships: [] - id: majima name: 真島香苗 - role: suspect publicIntroduction: "自信家の外部鑑定人。" personality: 自信家の外部鑑定人。来歴写真の差し替えを隠したいので今泉との鑑定上の衝突を小さく見せるが、21時台前半は別の資料室で作業していた。 goals: @@ -241,7 +259,6 @@ characters: strategy: maintain-until-contradicted memories: - id: door-open-long - about: vault-stayed-open detail: 21時10分すぎに廊下を通ったとき、希少資料庫の扉が資料搬入のため長く開けたままになっていたのを見た。 relationships: [] @@ -309,18 +326,17 @@ evidences: label: 希少資料庫の自動施錠仕様 description: 廊下側から開ける時だけ館長鍵が必要で、外へ出て扉を閉めるとラッチが自動で掛かる。施錠操作に鍵は不要である。 reveal: - mode: conversation - condition: 八神か戸塚に資料庫の鍵が開錠と施錠のどちらに必要なのか具体的に尋ねたら開示する。 + condition: 八神か戸塚に資料庫の鍵が開錠と施錠のどちらに必要なのか具体的に尋ねたら開示する。または希少資料庫の扉を調べ、閉じるだけで施錠されるラッチ構造を確認したら開示する。 sources: - { type: character, id: yagami } - { type: character, id: tozuka } + - { type: location, id: rare-vault } supports: [vault-key-only-opens, vault-self-locks, key-found-on-imaizumi] contradicts: ["lie:yagami-key-needed-to-lock"] - id: door-contact-log label: 二十一時九分から二十二分の扉記録 description: 資料庫の扉は21時09分に開き、21時22分に閉じるまで十三分間連続して開いていた。途中の再開錠は必要なかった。 reveal: - mode: conversation condition: 戸塚か真島に資料搬入中の扉の状態とセンサー記録について尋ねたら開示する。 sources: - { type: character, id: tozuka } @@ -331,7 +347,6 @@ evidences: label: 二十一時十四分の八神入室目撃 description: 久我は21時14分ごろ、搬入のため開いたままの資料庫へ八神が鍵を使わず入るのを見ている。 reveal: - mode: conversation condition: 久我に21時10分から20分ごろ資料庫前で見た人物を尋ねたら開示する。 sources: - { type: character, id: kuga } @@ -341,18 +356,17 @@ evidences: label: 修復処置記録と資料状態の不一致 description: 八神の処置記録では完了したはずの工程が資料の実状態と合わず、今泉が翌朝の担当変更と外部委員提出を記している。 reveal: - mode: conversation - condition: 八神か久我に今泉が事件直前に調べていた修復記録について尋ね、実資料との不一致を追及したら開示する。 + condition: 八神か久我に今泉が事件直前に調べていた修復記録について尋ね、実資料との不一致を追及したら開示する。または遺体・現場を調べ、「修復処置記録と資料状態の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: yagami } - { type: character, id: kuga } + - { type: victim, id: victim } supports: [yagami-forged-restoration-record, imaizumi-found-forgery, imaizumi-planned-review] contradicts: [] - id: kuga-damaged-document label: 久我が隠した損傷資料 description: 傷めた資料が久我の整理箱から見つかるが、希少資料庫の事件とは独立した不始末である。 reveal: - mode: conversation condition: 久我に整理中の資料を傷めて隠していないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: kuga } @@ -362,7 +376,6 @@ evidences: label: 戸塚の閲覧室カメラ停止記録 description: 戸塚が私用の読書を隠すため閲覧室カメラを一時停止していたことが分かるが、希少資料庫とは別区画である。 reveal: - mode: conversation condition: 戸塚に監視カメラを私的な理由で止めていないか尋ね、停止履歴を検証したら開示する。 sources: - { type: character, id: tozuka } @@ -372,7 +385,6 @@ evidences: label: 真島の来歴写真差し替え履歴 description: 真島が鑑定の誤りを隠すため提出写真を差し替えていたことが分かるが、今泉の死亡とは別件である。 reveal: - mode: conversation condition: 真島に来歴写真を差し替えていないか尋ね、提出データの履歴を検証したら開示する。 sources: - { type: character, id: majima } @@ -384,18 +396,9 @@ solution: summary: 犯人は八神琴子。修復失敗を隠すため処置記録を改ざんしていたことを今泉に見抜かれ、翌朝から重要資料の修復を外され外部委員へ記録を出される予定だった。21時09分、今泉が搬入のため希少資料庫を開け、扉は21時22分まで開いたままだった。八神は21時14分ごろ、その開いた扉から鍵を使わず資料庫へ入り、久我が目撃している。21時17分ごろ今泉も入り、鍵を自分のポケットへ戻した。八神は21時20分ごろ今泉を襲い、21時22分に廊下へ出て扉を閉めた。資料庫は閉めるだけで自動施錠されるため、開錠鍵を持っていなくても外から密室状況を作れた。鍵が今泉のポケットに残っていたことは、誰も出なかった証明ではない。 method: 搬入中に開いたままの資料庫へ鍵なしで入り、退出時は扉を閉めるだけで自動施錠される仕組みを利用して、鍵が被害者のポケットに残る密室状況を作った motive: 修復処置記録の改ざんが発覚し、翌朝から重要資料の担当を外され外部委員へ報告されることを恐れたため - requiredFacts: [yagami-forged-restoration-record, imaizumi-planned-review, vault-key-only-opens, vault-self-locks, vault-open-2109, vault-stayed-open, yagami-entered-vault-2114, kuga-saw-yagami-2114, yagami-killed-imaizumi, yagami-left-vault-2122, key-found-on-imaizumi] secretKeywords: - 犯人は八神 - 八神が犯人 - 八神が今泉を襲 - 私が今泉を襲 - 自動施錠で密室を作 -quality: - expectedQuestionCount: - min: 14 - max: 28 - requiredEvidence: - min: 3 - redHerrings: [kuga-tore-document, tozuka-disabled-camera, majima-hid-photo-swap] - notes: 『唯一の鍵が被害者のポケット』という強い密室記号を、開錠と施錠の非対称性で崩す。さらに扉が十三分間開放されていた記録と久我の入室目撃を組み合わせ、単なる仕様上の可能性ではなく八神の実際の侵入へ収束させる。 diff --git a/db/scenarios/flood-courthouse-unlogged-door.yaml b/db/scenarios/flood-courthouse-unlogged-door.yaml index 1ccd6d5..a1168ae 100644 --- a/db/scenarios/flood-courthouse-unlogged-door.yaml +++ b/db/scenarios/flood-courthouse-unlogged-door.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: flood-courthouse-unlogged-door meta: - title: 水没寸前の裁判所、資料棟の夜 - synopsis: "午後八時三十五分、河川氾濫により周辺道路が閉鎖された旧南央裁判所の資料棟で、主任記録官の磯崎章が死亡しているのが見つかりました。資料棟に残っていたのは書記官の矢吹梓、設備担当の三田村悟、警備員の木瀬亮の三人だけです。" + title: "旧南央裁判所殺人事件" + synopsis: "午後八時三十五分、河川氾濫により周辺道路が閉鎖された旧南央裁判所の資料棟で、主任記録官の磯崎章が死亡しているのが見つかりました。資料棟に\ + 残っていたのは書記官の矢吹梓、設備担当の三田村悟、警備員の木瀬亮の三人だけです。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [裁判所, 洪水, 電子錠, 密室] victim: name: 磯崎章 introduction: 旧南央裁判所主任記録官 + foundAt: 20:35 + foundIn: 保存記録室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 磯崎章は保存記録室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「監督部署への報告草案」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -53,83 +61,84 @@ facts: - id: yabuki-entered-unlogged statement: 20時09分ごろ、矢吹梓は職員証を使わず、開いたままの保存記録室へ入った kind: truth - secret: true - id: kise-saw-yabuki-return-2016 statement: 20時16分ごろ、木瀬亮は保存記録室側の廊下から戻る矢吹梓を見た kind: observation - id: isozaki-death-2012 statement: 20時12分ごろ、磯崎章は保存記録室で襲われ死亡した kind: truth - secret: true - id: yabuki-killed-isozaki statement: 矢吹梓は20時12分ごろ保存記録室で磯崎章を襲い死亡させた kind: truth - secret: true - id: altered-file-discovered statement: 磯崎章は矢吹梓が過去の裁判記録の閲覧処理を不適切に修正していたことに気づいた kind: motive - secret: true - id: report-next-morning statement: 磯崎章は翌朝、矢吹梓の記録修正について監督部署へ報告する予定だった kind: motive - secret: true - id: mitamura-skipped-check statement: 三田村悟は設備点検の一項目を実施せず、実施済みとして記録していた kind: other - secret: true - id: kise-read-private-file statement: 木瀬亮は警備権限では閲覧不要な古い事件資料を私的興味で読んでいた kind: other - secret: true - id: body-found-2035 statement: 20時35分、三田村悟が保存記録室で磯崎章の死を発見した kind: observation timeline: - id: door-opened-for-maintenance at: "19:50" - participants: [mitamura] - facts: [door-held-open-1950, door-remained-open-2022, no-new-unlock] + participants: [ mitamura ] + facts: [ door-held-open-1950, door-remained-open-2022, no-new-unlock ] + record: 扉状態記録 description: 設備点検の資材搬入のため、保存記録室の扉が開放状態になる。 + location: 保存記録室 - id: mitamura-warning at: "20:00" - participants: [mitamura, yabuki] - facts: [mitamura-told-yabuki-open] + participants: [ mitamura, yabuki ] + facts: [ mitamura-told-yabuki-open ] description: 三田村が矢吹に、保存記録室の扉が点検中で開いていることを伝える。 + location: 保存記録室 - id: yabuki-enters at: "20:09" - participants: [yabuki] - facts: [yabuki-entered-unlogged] + participants: [ yabuki ] + facts: [ yabuki-entered-unlogged ] description: 矢吹が職員証を使わず、開放中の保存記録室へ入る。 + location: 保存記録室 - id: isozaki-death at: "20:12" - participants: [yabuki] - facts: [isozaki-death-2012, yabuki-killed-isozaki] + participants: [ yabuki ] + facts: [ isozaki-death-2012, yabuki-killed-isozaki ] description: 矢吹が保存記録室で磯崎を襲い、磯崎は死亡する。 + location: 保存記録室 - id: corridor-return at: "20:16" - participants: [yabuki, kise] - facts: [kise-saw-yabuki-return-2016] + participants: [ yabuki, kise ] + facts: [ kise-saw-yabuki-return-2016 ] description: 木瀬が保存記録室側の廊下から戻る矢吹を目撃する。 + location: 廊下 - id: door-closes at: "20:22" - participants: [mitamura] - facts: [door-remained-open-2022] + participants: [ mitamura ] + facts: [ door-remained-open-2022 ] + record: 扉状態記録 description: 設備点検が終わり、保存記録室の扉が通常の施錠状態へ戻る。 + location: 保存記録室 - id: discovery at: "20:35" - participants: [mitamura, yabuki, kise] - facts: [body-found-2035] + participants: [ mitamura, yabuki, kise ] + facts: [ body-found-2035 ] description: 三田村が保存記録室で磯崎の死を発見する。 + location: 保存記録室 characters: - id: yabuki name: 矢吹梓 - role: suspect publicIntroduction: "裁判所の書記官。" personality: 論理的で、規則やシステムの仕様を根拠に話す書記官。記録上の証拠を重視するため、自分に有利なログだけを強調する傾向がある。 goals: - 解錠履歴がないことを、自分が保存記録室へ入っていない証明として通したい - 過去の裁判記録を不適切に修正していたことを隠したい - knowledge: [yabuki-clerk, no-new-unlock, door-held-open-1950, body-found-2035] + knowledge: [ yabuki-clerk, no-new-unlock, door-held-open-1950, body-found-2035 ] secrets: - fact: yabuki-entered-unlogged disclosure: never @@ -150,10 +159,8 @@ characters: strategy: maintain-until-contradicted memories: - id: open-door-told - about: mitamura-told-yabuki-open detail: 三田村から点検中で扉が開いていると聞いた瞬間、職員証を使わず中へ入れる時間があると理解した。 - id: report-threat - about: report-next-morning detail: 磯崎から「明朝、監督部署に記録修正の経緯を出す」と告げられ、処分を避けられないと感じた。 relationships: - character: mitamura @@ -164,13 +171,20 @@ characters: attitude: 廊下で人をよく見ているので苦手 - id: mitamura name: 三田村悟 - role: witness publicIntroduction: "設備仕様には詳しいが、書類仕事が雑な技術職員。" personality: 設備仕様には詳しいが、書類仕事が雑な技術職員。点検の一項目を飛ばしたことを隠したい一方、電子錠が何を記録するかは正確に説明できる。 goals: - 点検項目を飛ばしたことを隠したい - 電子錠のログ仕様について誤解を解きたい - knowledge: [mitamura-facility, access-log-unlock-only, door-held-open-1950, door-remained-open-2022, mitamura-told-yabuki-open, body-found-2035] + knowledge: + [ + mitamura-facility, + access-log-unlock-only, + door-held-open-1950, + door-remained-open-2022, + mitamura-told-yabuki-open, + body-found-2035 + ] secrets: - fact: mitamura-skipped-check disclosure: pressured @@ -181,7 +195,6 @@ characters: strategy: maintain-until-contradicted memories: - id: explained-open-door - about: mitamura-told-yabuki-open detail: 矢吹に「二十分くらい扉を開けているから、資料を取るなら今ならカードはいらない」と話したのを覚えている。 relationships: - character: yabuki @@ -189,13 +202,12 @@ characters: attitude: ログの意味を自分に都合よく解釈していると感じる - id: kise name: 木瀬亮 - role: witness publicIntroduction: "寡黙な警備員。" personality: 寡黙な警備員。人の歩き方や服装をよく覚えているが、勤務中に古い事件資料を私的に読んでいたことを知られたくない。 goals: - 私的に事件資料を読んでいたことを隠したい - 20時16分の矢吹の目撃については正確に話したい - knowledge: [kise-security, kise-saw-yabuki-return-2016, body-found-2035] + knowledge: [ kise-security, kise-saw-yabuki-return-2016, body-found-2035 ] secrets: - fact: kise-read-private-file disclosure: pressured @@ -206,7 +218,6 @@ characters: strategy: maintain-until-contradicted memories: - id: yabuki-return - about: kise-saw-yabuki-return-2016 detail: 20時16分ごろ、保存記録室側から早足で戻る矢吹と正面ですれ違ったので、見間違いではない。 relationships: - character: yabuki @@ -226,14 +237,20 @@ revelations: revealCondition: 三田村に電子錠が何を記録するのかと、19時50分からの点検中の扉状態を尋ねた。 requires: revelations: [] - evidences: [door-state-log] + evidences: [ door-state-log ] - type: character id: kise revealCondition: 木瀬に20時台の保存記録室側廊下で見た人物を尋ね、ログがなくても矢吹が廊下から戻ってきたことを確認した。 requires: revelations: [] - evidences: [corridor-witness] - relatedFacts: [access-log-unlock-only, door-held-open-1950, door-remained-open-2022, yabuki-entered-unlogged] + evidences: [ corridor-witness ] + relatedFacts: + [ + access-log-unlock-only, + door-held-open-1950, + door-remained-open-2022, + yabuki-entered-unlogged + ] - id: altered-record-motive title: 翌朝の記録修正報告 text: 磯崎は矢吹による不適切な裁判記録修正を発見し、翌朝監督部署へ報告する予定だった。矢吹には処分を避けたい動機があった。 @@ -246,97 +263,84 @@ revelations: id: yabuki revealCondition: 矢吹に磯崎が見つけた記録修正と翌朝の報告予定を具体的に追及した。 requires: - revelations: [log-records-unlock-not-entry] - evidences: [report-draft] - relatedFacts: [altered-file-discovered, report-next-morning] + revelations: [ log-records-unlock-not-entry ] + evidences: [ report-draft ] + relatedFacts: [ altered-file-discovered, report-next-morning ] evidences: - id: door-state-log label: 保存記録室の扉状態記録 description: 19時50分から20時22分まで扉は完全には閉じておらず、職員証を使わず通過できる状態だった。 reveal: - mode: conversation condition: 三田村に点検中の保存記録室の扉状態と電子錠ログの仕様を尋ねたら開示する。 sources: - type: character id: mitamura - supports: [access-log-unlock-only, door-held-open-1950, door-remained-open-2022] - contradicts: ["lie:yabuki-log-alibi"] + supports: [ access-log-unlock-only, door-held-open-1950, door-remained-open-2022 ] + contradicts: [ "lie:yabuki-log-alibi" ] - id: maintenance-warning label: 点検中の開放を伝えた記録 description: 三田村は20時ごろ、矢吹本人に保存記録室の扉が開いていると伝えていた。 reveal: - mode: conversation condition: 三田村か矢吹に20時ごろ交わした保存記録室の点検について尋ねたら開示する。 sources: - type: character id: mitamura - type: character id: yabuki - supports: [mitamura-told-yabuki-open] + supports: [ mitamura-told-yabuki-open ] contradicts: [] - id: corridor-witness label: 二十時十六分の廊下目撃 description: 木瀬は保存記録室側の廊下から戻る矢吹を20時16分ごろに目撃している。 reveal: - mode: conversation condition: 木瀬に20時10分から20分の間に保存記録室側で誰を見たか尋ねたら開示する。 sources: - type: character id: kise - supports: [kise-saw-yabuki-return-2016] - contradicts: ["lie:yabuki-no-corridor", "lie:yabuki-log-alibi"] + supports: [ kise-saw-yabuki-return-2016 ] + contradicts: [ "lie:yabuki-no-corridor", "lie:yabuki-log-alibi" ] - id: report-draft label: 監督部署への報告草案 description: 磯崎の端末に、矢吹による不適切な記録修正を翌朝報告するための草案が残っている。 reveal: - mode: conversation - condition: 矢吹か三田村に磯崎が翌朝提出予定だった報告について尋ねたら開示する。 + condition: 矢吹か三田村に磯崎が翌朝提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「監督部署への報告草案」に関わる資料を確認したら開示する。 sources: - type: character id: yabuki - type: character id: mitamura - supports: [altered-file-discovered, report-next-morning] + - type: victim + id: victim + supports: [ altered-file-discovered, report-next-morning ] contradicts: [] - id: skipped-maintenance-item label: 未実施の点検項目 description: 三田村が点検の一項目を実施せず済ませたことが分かるが、保存記録室への矢吹の出入りとは独立している。 reveal: - mode: conversation condition: 三田村に点検記録の空白を具体的に追及したら開示する。 sources: - type: character id: mitamura - supports: [mitamura-skipped-check] - contradicts: ["lie:mitamura-full-check"] + supports: [ mitamura-skipped-check ] + contradicts: [ "lie:mitamura-full-check" ] - id: private-file-reading label: 警備員の閲覧履歴 description: 木瀬が私的興味で古い事件資料を読んでいたことが分かるが、磯崎の死とは結びつかない。 reveal: - mode: conversation condition: 木瀬に勤務中に閲覧していた資料について追及したら開示する。 sources: - type: character id: kise - supports: [kise-read-private-file] - contradicts: ["lie:kise-no-private-reading"] + supports: [ kise-read-private-file ] + contradicts: [ "lie:kise-no-private-reading" ] solution: culprit: yabuki summary: 犯人は矢吹梓。保存記録室のアクセス履歴は「誰が扉を通ったか」ではなく「職員証で解錠した操作」だけを記録する。19時50分から20時22分まで扉は設備点検のため開放されており、矢吹は20時ごろ三田村からそのことを直接聞いていた。矢吹は20時09分ごろ職員証を使わず記録室へ入り、20時12分ごろ磯崎を襲った。20時16分には木瀬が記録室側の廊下から戻る矢吹を目撃している。磯崎は矢吹による不適切な裁判記録修正を発見し、翌朝監督部署へ報告する予定だった。解錠履歴の欠如を入室不可能の証明にすり替えたことがアリバイ工作の核心である。 method: 点検中で扉が開放されている時間に職員証を使わず保存記録室へ入り、解錠履歴が残らないことを利用して不在を装った motive: 不適切な裁判記録修正を磯崎が翌朝監督部署へ報告する予定で、処分を受けることを恐れたため - requiredFacts: [access-log-unlock-only, door-remained-open-2022, mitamura-told-yabuki-open, yabuki-entered-unlogged, kise-saw-yabuki-return-2016, report-next-morning, yabuki-killed-isozaki] secretKeywords: - 犯人は矢吹 - 矢吹が犯人 - 矢吹が磯崎を襲 - 私が磯崎を襲 - 解錠履歴をアリバイに -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [mitamura-skipped-check, kise-read-private-file] - notes: 「ログがない=通過していない」という前提を崩す密室型。仕様を知るだけでなく、矢吹本人が扉の開放を事前に知っていたことと廊下目撃を合わせて意図性を確定する。 diff --git a/db/scenarios/gallery-blue-frame.yaml b/db/scenarios/gallery-blue-frame.yaml index fa121ce..f6eb439 100644 --- a/db/scenarios/gallery-blue-frame.yaml +++ b/db/scenarios/gallery-blue-frame.yaml @@ -1,15 +1,33 @@ schemaVersion: 1 id: gallery-blue-frame meta: - title: 青環美術館、内覧会の夜 + title: "内覧会のあと、絵は黙る" synopsis: "午後六時五十五分、市立青環美術館の収蔵庫前で、主任学芸員の鳥羽薫が死亡しているのが見つかりました。その夜は翌日から始まる企画展の内覧会で、一般客が帰った後も数人の関係者が館内に残っていました。" category: 美術館ミステリ difficulty: 3 estimatedMinutes: 10 - tags: [美術館, 贋作, 記録] victim: name: 鳥羽薫 introduction: 市立青環美術館主任学芸員 + foundAt: 18:55 + foundIn: 収蔵庫前 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 鳥羽薫は収蔵庫前で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「鳥羽の額装検査メモ」に関わる資料が残されている。 +places: + - id: restoration-room + name: 修復室 + shortName: 修復室 + introduction: 紫外線検査装置と修復用の作業台がある部屋 + situation: 検査装置は停止し、作業台だけが照明に浮かんでいる + findings: + - id: uv-power-gap + statement: 紫外線検査装置の履歴には、18時37分から18時51分まで電源が切れていた空白がある。 + - id: uv-resume-time + statement: 装置は18時51分に再起動しており、その前の検査状態が連続していたわけではない。 briefing: |- ——事件の記録を読み上げます。 @@ -38,105 +56,106 @@ facts: - id: sakaki-made-forgery statement: 榊玲は数か月前、修復を任された作品の一枚を精巧な贋作と入れ替え、本物を外部へ流そうとしていた kind: motive - secret: true - id: toba-found-forgery statement: 鳥羽薫は事件当日の18時25分ごろ、額装と絵具層の違いから作品のすり替えに気づいた kind: motive - secret: true - id: toba-confronted-sakaki statement: 18時29分ごろ、鳥羽薫は榊玲に贋作の件を問いただし、翌朝館長と警察へ報告すると告げた kind: motive - secret: true - id: mido-offered-bribe statement: 18時32分ごろ、御堂和也は別作品の評価額を高く見せるため、鳥羽薫へ謝礼を持ちかけて拒否された kind: motive - secret: true - id: uv-test-started statement: 18時35分、榊玲は修復室の紫外線検査装置を起動した kind: physical - id: uv-lamp-off-1837 statement: 18時37分から18時51分まで、修復室の紫外線検査装置は電源が切れていた kind: physical - secret: true - id: sakaki-left-restoration statement: 18時38分ごろ、榊玲は修復室を離れて収蔵庫側へ向かった kind: truth - secret: true - id: enomoto-saw-sakaki-1844 statement: 18時44分ごろ、榎本駿は収蔵庫前の通路で榊玲を見た kind: observation - id: sakaki-killed-toba-1846 statement: 18時46分ごろ、榊玲は収蔵庫前で鳥羽薫を襲い死亡させた kind: truth - secret: true - id: sakaki-returned-1850 statement: 18時50分ごろ、榊玲は修復室へ戻った kind: truth - secret: true - id: uv-test-resumed statement: 18時51分、修復室の紫外線検査装置が再び起動した kind: physical - id: enomoto-dozed statement: 榎本駿は18時35分から18時42分ごろまで警備控室で短時間居眠りしていた kind: other - secret: true - id: body-found-1855 statement: 18時55分、御堂和也が収蔵庫前で鳥羽薫の死を発見した kind: observation - id: genuine-painting-hidden statement: すり替えられた本物の絵画は、榊玲が契約していた外部保管庫から後に見つかった kind: physical - secret: true timeline: - id: forgery-discovered at: "18:25" - participants: [sakaki] + participants: [] facts: [toba-found-forgery] description: 鳥羽が作品のすり替えに気づく。 + location: 美術館内 - id: sakaki-confronted at: "18:29" participants: [sakaki] facts: [toba-confronted-sakaki] description: 鳥羽が榊へ贋作の件を問いただし、翌朝の報告を告げる。 + location: 美術館内 - id: mido-bribe at: "18:32" participants: [mido] facts: [mido-offered-bribe] description: 御堂が作品評価を巡る謝礼を鳥羽へ持ちかけ、拒否される。 + location: 美術館内 - id: uv-test-start at: "18:35" participants: [sakaki] facts: [uv-test-started] + record: 装置電源履歴 description: 榊が修復室で紫外線検査装置を起動する。 + location: 修復室 - id: sakaki-leaves at: "18:38" participants: [sakaki] facts: [uv-lamp-off-1837, sakaki-left-restoration] + record: 装置電源履歴 description: 装置の電源が切れた後、榊が修復室を離れて収蔵庫側へ向かう。 + location: 修復室 - id: enomoto-sighting at: "18:44" participants: [sakaki, enomoto] facts: [enomoto-saw-sakaki-1844] description: 榎本が収蔵庫前の通路で榊を目撃する。 + location: 収蔵庫前 - id: toba-death at: "18:46" participants: [sakaki] facts: [sakaki-killed-toba-1846] description: 榊が収蔵庫前で鳥羽を襲い、鳥羽は死亡する。 + location: 収蔵庫前 - id: sakaki-returns at: "18:50" participants: [sakaki] facts: [sakaki-returned-1850, uv-test-resumed] + record: 装置電源履歴 description: 榊が修復室へ戻り、ほどなく紫外線検査を再開する。 + location: 修復室 - id: discovery at: "18:55" participants: [mido, sakaki, enomoto] facts: [body-found-1855] description: 御堂が収蔵庫前で鳥羽の死を発見する。 + location: 収蔵庫前 characters: - id: sakaki name: 榊玲 - role: suspect publicIntroduction: "物静かで職人気質の修復士。" personality: 物静かで職人気質の修復士。作品の状態については細部まで説明するが、自分の判断を疑われることを極端に嫌う。鳥羽には技術を評価されていた一方、管理の厳しさを窮屈に感じていた。 goals: @@ -165,7 +184,6 @@ characters: strategy: maintain-until-contradicted memories: - id: forgery-confrontation - about: toba-confronted-sakaki detail: 鳥羽が額縁を机に置き「明日の朝、館長にも警察にも話す」と言ったとき、手が冷たくなったのを覚えている。 relationships: - character: enomoto @@ -173,7 +191,6 @@ characters: attitude: 警備の目は細かいので少し苦手 - id: mido name: 御堂和也 - role: suspect publicIntroduction: "洗練された物腰の画商。" personality: 洗練された物腰の画商。作品の価値と人間関係を同じように交渉材料として見る癖がある。鳥羽に不適切な提案を拒絶されたため、その面会自体を消したがっている。 goals: @@ -190,12 +207,10 @@ characters: strategy: maintain-until-contradicted memories: - id: rejected-bribe - about: mido-offered-bribe detail: 鳥羽に封筒を押し返され「作品の値段はあなたが決めるものじゃない」と言われた屈辱を覚えている。 relationships: [] - id: enomoto name: 榎本駿 - role: witness publicIntroduction: "口数の少ない警備員。" personality: 口数の少ない警備員。巡回経路と時刻はよく覚えているが、短時間居眠りしたことを処分の対象にされるのを恐れている。見た人物を必要以上に断定しない慎重さがある。 goals: @@ -212,7 +227,6 @@ characters: strategy: maintain-until-contradicted memories: - id: blue-frame-corridor - about: enomoto-saw-sakaki-1844 detail: 収蔵庫前で、青い保護枠を抱えた榊とすれ違った。修復室にいるはずだと思っていたので時刻まで覚えている。 relationships: - character: sakaki @@ -254,20 +268,19 @@ evidences: label: 紫外線検査装置の電源履歴 description: 装置は18時35分に起動したが、18時37分から18時51分まで停止している。 reveal: - mode: conversation - condition: 榊に検査を続けていた時間帯を尋ねるか、榎本に修復室の機器ログを確認できないか尋ねたら開示する。 + condition: 榊に検査を続けていた時間帯を尋ねるか、榎本に修復室の機器ログを確認できないか尋ねたら開示する。または修復室を調べ、紫外線検査装置の電源履歴を確認したら開示する。 sources: - type: character id: sakaki - type: character id: enomoto + - { type: location, id: restoration-room } supports: [uv-test-started, uv-lamp-off-1837, uv-test-resumed] contradicts: ["lie:sakaki-uv-alibi"] - id: enomoto-sighting label: 十八時四十四分の収蔵庫前の目撃 description: 榎本は18時44分ごろ、収蔵庫前の通路で榊を見ている。 reveal: - mode: conversation condition: 榎本に18時40分台の巡回で誰とすれ違ったか尋ね、榊の姿を見たと答えたら開示する。 sources: - type: character @@ -278,20 +291,20 @@ evidences: label: 鳥羽の額装検査メモ description: 展示予定作品の額装と絵具層が過去の記録と一致せず、鳥羽が「原画ではない可能性」と書き残している。 reveal: - mode: conversation - condition: 榊か御堂に鳥羽が事件前に作品の真贋を調べていなかったか尋ね、額装の違いへ話が及んだら開示する。 + condition: 榊か御堂に鳥羽が事件前に作品の真贋を調べていなかったか尋ね、額装の違いへ話が及んだら開示する。または遺体・現場を調べ、「鳥羽の額装検査メモ」に関わる資料を確認したら開示する。 sources: - type: character id: sakaki - type: character id: mido + - type: victim + id: victim supports: [toba-found-forgery, sakaki-made-forgery] contradicts: [] - id: mido-envelope label: 御堂の謝礼額を書いたメモ description: 御堂が鳥羽へ提示した謝礼額のメモ。拒否されたことは分かるが、事件時刻の行動とは結びつかない。 reveal: - mode: conversation condition: 御堂に鳥羽との会話内容を問い、金銭の話を否定したため商談資料を確認したら開示する。 sources: - type: character @@ -302,7 +315,6 @@ evidences: label: 警備控室端末の無操作記録 description: 18時35分から18時42分ごろまで榎本の端末操作がなく、本人も短時間の居眠りを認めるが、18時44分の目撃とは両立する。 reveal: - mode: conversation condition: 榎本に巡回が途切れた時間がないか確認し、端末操作の空白を示したら開示する。 sources: - type: character @@ -314,18 +326,9 @@ solution: summary: 犯人は榊玲。作品のすり替えを鳥羽に見抜かれ、翌朝館長と警察へ報告すると告げられた。榊は18時35分に紫外線検査を始めたが、18時37分に装置を止めて修復室を離れた。18時44分には榎本が収蔵庫前で榊を見ており、18時46分ごろ鳥羽を襲った後、18時50分ごろ修復室へ戻って18時51分に検査装置を再起動した。「ずっと検査をしていた」という榊のアリバイは機器の電源履歴と榎本の目撃の二方向から崩れる。御堂の不適切な謝礼提案と榎本の居眠りは、事件と切り離された秘密である。 method: 紫外線検査装置を一度停止して修復室を離れ、収蔵庫前で鳥羽を襲った後に戻って検査を再開し、連続作業に見せかけた motive: 絵画のすり替えと本物の持ち出しが翌朝発覚し、職業上の立場を失うことを恐れたため - requiredFacts: [sakaki-made-forgery, toba-confronted-sakaki, uv-lamp-off-1837, sakaki-left-restoration, enomoto-saw-sakaki-1844, sakaki-killed-toba-1846, sakaki-returned-1850] secretKeywords: - 犯人は榊 - 榊が犯人 - 榊が鳥羽を襲 - 私が鳥羽を襲 - 贋作発覚で殺 -quality: - expectedQuestionCount: - min: 8 - max: 18 - requiredEvidence: - min: 2 - redHerrings: [mido-offered-bribe, enomoto-dozed] - notes: 主経路は装置の停止記録と榎本の目撃で榊の「連続検査」アリバイを崩すこと。御堂と榎本の嘘も証拠で崩れるが、どちらも殺害時刻へは繋げない。 diff --git a/db/scenarios/generation-ship-staggered-dawn.yaml b/db/scenarios/generation-ship-staggered-dawn.yaml index f2496d5..ceec302 100644 --- a/db/scenarios/generation-ship-staggered-dawn.yaml +++ b/db/scenarios/generation-ship-staggered-dawn.yaml @@ -1,15 +1,24 @@ schemaVersion: 1 id: generation-ship-staggered-dawn meta: - title: 世代船アステリア、居住リングの夜 - synopsis: "2312年、恒星間世代船《アステリア》。外部航行中のため、居住リングから船外へ出ることはできません。 生態系主任ミラ・ヴォスが種子保管区で死亡しました。" + title: "世代船アステリア事件" + synopsis: "2312年、恒星間世代船《アステリア》。外部航行中のため、居住リングから船外へ出ることはできません。 + 生態系主任ミラ・ヴォスが種子保管区で死亡しました。" category: SFクローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [2312年, 世代船, 人工昼夜, 時刻] victim: name: ミラ・ヴォス introduction: 恒星間世代船《アステリア》生態系主任 + foundAt: 06:28 + foundIn: 種子保管区 + estimatedDeathAt: "05:57" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: ミラ・ヴォスは種子保管区で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「希少種子の監査記録」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -50,77 +59,85 @@ facts: - id: sera-seed-diversion statement: セラ・イワノフは希少種子を非公式な私的栽培区へ回していた kind: motive - secret: true - id: mira-found-diversion statement: ミラ・ヴォスは事件当日、セラによる希少種子の流用を発見した kind: motive - secret: true - id: mira-would-audit statement: 船内標準時05時45分、ミラはセラに中央評議会へ流用を報告すると告げた kind: motive - secret: true - id: dario-saw-sera-0552 statement: 船内標準時05時52分ごろ、ダリオ・ケインは中央区から種子保管区へ向かう連絡路でセラを見た kind: observation - id: sera-killed-mira statement: 船内標準時05時57分ごろ、セラ・イワノフは種子保管区でミラを襲い死亡させた kind: truth - secret: true - id: sera-claimed-after-six statement: セラ・イワノフは農業区の人工06時05分からずっと農業区にいたと主張した kind: testimony - id: dario-hidden-overload statement: ダリオ・ケインは規定を超える電力を個人工作室へ割り当てていた kind: other - secret: true - id: yuna-private-medication statement: ユナ・パクは手続き外で友人へ医療用品を融通していた kind: other - secret: true - id: body-found-0628 statement: 船内標準時06時28分、ユナ・パクが種子保管区でミラの死を発見した kind: observation timeline: - id: mira-confronts-sera at: "05:45" - participants: [sera] - facts: [sera-seed-diversion, mira-found-diversion, mira-would-audit] + participants: [ sera ] + facts: [ sera-seed-diversion, mira-found-diversion, mira-would-audit ] description: ミラが希少種子の流用をセラへ突きつけ、中央評議会への報告を告げる。 + location: 船内 - id: dario-sees-sera at: "05:52" - participants: [sera, dario] - facts: [dario-saw-sera-0552] + participants: [ sera, dario ] + facts: [ dario-saw-sera-0552 ] description: ダリオが種子保管区へ向かう連絡路でセラを見かける。 + location: 連絡路 - id: mira-death at: "05:57" - participants: [sera] - facts: [sera-killed-mira] + participants: [ sera ] + facts: [ sera-killed-mira ] description: セラが種子保管区でミラを襲う。 + location: 種子保管区 - id: agriculture-six at: "06:00" - participants: [sera] - facts: [agriculture-dawn-0600-local, sera-claimed-after-six] + participants: [ sera ] + facts: [ agriculture-dawn-0600-local, sera-claimed-after-six ] + record: 区画昼夜表 description: 船内標準時ではすでに06時00分だが、セラは農業区の人工06時05分を基準に自分の行動を説明する。 + location: 農業区 - id: medical-six at: "06:20" - participants: [yuna] - facts: [medical-dawn-0600-local] + participants: [ yuna ] + facts: [ medical-dawn-0600-local ] + record: 区画昼夜表 description: 医療区が人工06時00分を迎える。 + location: 医療区 - id: discovery at: "06:28" - participants: [sera, dario, yuna] - facts: [body-found-0628] + participants: [ sera, dario, yuna ] + facts: [ body-found-0628 ] description: ユナが種子保管区でミラの死を発見する。 + location: 種子保管区 characters: - id: sera name: セラ・イワノフ - role: suspect publicIntroduction: "有能な農業主任。" personality: 有能な農業主任。自分の区画時間で物事を話す癖があり、「六時過ぎには農業区にいた」と繰り返す。希少種子の管理を問われると強く反発する。 goals: - 希少種子の流用を隠したい - 農業区の人工06時を事件時刻より後だと思わせたい - knowledge: [sera-agriculture, staggered-circadian-cycles, agriculture-dawn-0600-local, sera-claimed-after-six, body-found-0628] + knowledge: + [ + sera-agriculture, + staggered-circadian-cycles, + agriculture-dawn-0600-local, + sera-claimed-after-six, + body-found-0628 + ] secrets: - fact: sera-seed-diversion disclosure: pressured @@ -141,7 +158,6 @@ characters: strategy: maintain-until-contradicted memories: - id: sera-audit-threat - about: mira-would-audit detail: ミラに「中央区が朝になったら評議会へ送る」と言われ、時間がないと感じた。 relationships: - character: dario @@ -152,13 +168,21 @@ characters: attitude: 医療区の時間感覚が自分とずれているので話が噛み合わないことが多い - id: dario name: ダリオ・ケイン - role: witness publicIntroduction: "電力管制官。" personality: 船内標準時で考える電力管制官。区画時間を標準時へ換算するのが習慣になっている。個人工作室への電力流用は隠したい。 goals: - 個人工作室への規定外電力を隠したい - 05時52分のセラの目撃を標準時で正確に伝えたい - knowledge: [dario-power, staggered-circadian-cycles, agriculture-dawn-0600-local, central-dawn-0600-local, medical-dawn-0600-local, dario-saw-sera-0552, body-found-0628] + knowledge: + [ + dario-power, + staggered-circadian-cycles, + agriculture-dawn-0600-local, + central-dawn-0600-local, + medical-dawn-0600-local, + dario-saw-sera-0552, + body-found-0628 + ] secrets: - fact: dario-hidden-overload disclosure: pressured @@ -169,7 +193,6 @@ characters: strategy: maintain-until-contradicted memories: - id: dario-sera-crossing - about: dario-saw-sera-0552 detail: 標準時05時52分、農業区の朝表示なら06時12分ごろのセラと連絡路ですれ違った。 relationships: - character: sera @@ -180,13 +203,18 @@ characters: attitude: 医療区時間で話すので換算しないと混乱する - id: yuna name: ユナ・パク - role: suspect publicIntroduction: "穏やかな医療技師。" personality: 穏やかな医療技師。患者の生活周期に合わせて医療区時間で話す。友人へ医療用品を融通した件は隠したい。 goals: - 手続き外の医療用品融通を隠したい - 自分が発見した時刻を標準時へ正しく直したい - knowledge: [yuna-medical, staggered-circadian-cycles, medical-dawn-0600-local, body-found-0628] + knowledge: + [ + yuna-medical, + staggered-circadian-cycles, + medical-dawn-0600-local, + body-found-0628 + ] secrets: - fact: yuna-private-medication disclosure: pressured @@ -197,8 +225,9 @@ characters: strategy: maintain-until-contradicted memories: - id: yuna-discovery-clock - about: body-found-0628 detail: 医療区時計では06時08分だったが、標準時表示では06時28分だったのを覚えている。 + - id: death-estimate-memory + detail: 医療担当として発見時の状態を確認し、死亡は船内標準時05時57分ごろと見積もっている。人工昼夜表示ではなく標準時で記録した。 relationships: - character: sera relation: 同僚 @@ -218,8 +247,14 @@ revelations: revealCondition: ダリオに三つの区画の06時00分を船内標準時へ換算してもらった。 requires: revelations: [] - evidences: [circadian-schedule] - relatedFacts: [staggered-circadian-cycles, agriculture-dawn-0600-local, central-dawn-0600-local, medical-dawn-0600-local] + evidences: [ circadian-schedule ] + relatedFacts: + [ + staggered-circadian-cycles, + agriculture-dawn-0600-local, + central-dawn-0600-local, + medical-dawn-0600-local + ] - id: sera-alibi-shifts title: セラの「六時過ぎ」は事件前後を隠す text: セラが農業区06時05分と言う時刻は船内標準時05時45分に相当し、事件時刻より前から始まる幅広い表現だった。標準時05時52分には種子保管区へ向かう姿を見られている。 @@ -230,65 +265,73 @@ revelations: id: dario revealCondition: 区画時間の差を確認した後、セラの06時05分を標準時へ換算し、05時52分の目撃と並べた。 requires: - revelations: [three-sixes] - evidences: [dario-route-log] - relatedFacts: [sera-claimed-after-six, dario-saw-sera-0552] + revelations: [ three-sixes ] + evidences: [ dario-route-log ] + relatedFacts: [ sera-claimed-after-six, dario-saw-sera-0552 ] evidences: - id: circadian-schedule label: 区画別の人工昼夜スケジュール description: 農業区06時00分は標準時05時40分、中央区06時00分は06時00分、医療区06時00分は06時20分に対応する。 reveal: - mode: conversation condition: ダリオかユナに区画ごとの人工昼夜が同時か尋ね、標準時への換算表を確認したら開示する。 sources: - { type: character, id: dario } - { type: character, id: yuna } - supports: [staggered-circadian-cycles, agriculture-dawn-0600-local, central-dawn-0600-local, medical-dawn-0600-local] - contradicts: ["lie:sera-six-alibi"] + supports: + [ + staggered-circadian-cycles, + agriculture-dawn-0600-local, + central-dawn-0600-local, + medical-dawn-0600-local + ] + contradicts: [ "lie:sera-six-alibi" ] - id: dario-route-log label: 電力巡回の位置記録 description: ダリオの巡回記録と本人の証言から、標準時05時52分に種子保管区へ向かうセラを見たことが確認できる。 reveal: - mode: conversation condition: ダリオに標準時05時台の巡回中に誰を見たか尋ねたら開示する。 sources: - { type: character, id: dario } - supports: [dario-saw-sera-0552] - contradicts: ["lie:sera-six-alibi"] + supports: [ dario-saw-sera-0552 ] + contradicts: [ "lie:sera-six-alibi" ] - id: seed-audit label: 希少種子の監査記録 description: セラ管理の希少種子だけ在庫と割当先が合わず、ミラが中央評議会への報告を準備している。 reveal: - mode: conversation - condition: セラかダリオにミラが事件直前に確認していた種子在庫を尋ねたら開示する。 + condition: セラかダリオにミラが事件直前に確認していた種子在庫を尋ねたら開示する。または遺体・現場を調べ、「希少種子の監査記録」に関わる資料を確認したら開示する。 sources: - { type: character, id: sera } - { type: character, id: dario } - supports: [sera-seed-diversion, mira-found-diversion, mira-would-audit] - contradicts: ["lie:sera-no-diversion"] + - { type: victim, id: victim } + supports: [ sera-seed-diversion, mira-found-diversion, mira-would-audit ] + contradicts: [ "lie:sera-no-diversion" ] - id: yuna-private-supply label: 手続き外の医療用品 description: ユナが友人へ融通した用品の記録が見つかるが、種子保管区の事件とは独立している。 reveal: - mode: conversation condition: ユナに在庫差について追及したら開示する。 sources: - { type: character, id: yuna } - supports: [yuna-private-medication] - contradicts: ["lie:yuna-no-private-supply"] + supports: [ yuna-private-medication ] + contradicts: [ "lie:yuna-no-private-supply" ] + - id: death-estimate + label: 医療区の死亡推定 + description: 医療センサーによる発見時の確認では、ミラの死亡は船内標準時05時57分ごろと見積もられる。区画ごとの人工時刻とは別の基準である。 + reveal: + condition: 遺体を調べて医療センサーの確認値を見るか、医療担当のユナに死亡推定を船内標準時で尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: yuna } + supports: [sera-killed-mira] + contradicts: [] + revealsDeathTime: true solution: culprit: sera summary: 犯人はセラ・イワノフ。希少種子の流用をミラに見抜かれ、中央評議会へ報告されるのを恐れた。セラは農業区の人工時刻を使って「06時05分には農業区にいた」と語り、聞き手が船内標準時の06時05分だと思うよう誘導した。しかし農業区の06時00分は標準時05時40分で、農業区06時05分は標準時05時45分にすぎない。標準時05時52分にはダリオが種子保管区へ向かうセラを目撃し、05時57分ごろ事件が起きている。区画ごとの複数の朝を一つの時系列へ換算するとアリバイは消える。 method: 区画ごとにずれた人工昼夜時刻を同一の時計時刻のように語り、事件時刻との前後関係を誤認させた motive: 希少種子の不正流用が中央評議会へ報告されるのを防ぐため - requiredFacts: [staggered-circadian-cycles, agriculture-dawn-0600-local, dario-saw-sera-0552, sera-seed-diversion, mira-would-audit, sera-killed-mira, sera-claimed-after-six] secretKeywords: - 犯人はセラ - セラがミラを襲 - 区画時間でアリバイ - 私がミラを襲 -quality: - expectedQuestionCount: { min: 10, max: 22 } - requiredEvidence: { min: 3 } - redHerrings: [dario-hidden-overload, yuna-private-medication] - notes: 同じ「06時」という自然言語が別の標準時を指す未来社会の時刻トリック。換算表と独立した目撃を組み合わせて解く。 diff --git a/db/scenarios/glasshouse-dawn-irrigation.yaml b/db/scenarios/glasshouse-dawn-irrigation.yaml index 7dcd449..9c1a666 100644 --- a/db/scenarios/glasshouse-dawn-irrigation.yaml +++ b/db/scenarios/glasshouse-dawn-irrigation.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: glasshouse-dawn-irrigation meta: - title: 緑苑植物園、開園前の事件 + title: "花が起きる前に" synopsis: "午前六時三十分、市立緑苑植物園の標本庫で、主任研究員の木島祥子が死亡しているのが見つかりました。開園前の園内にいたのは、研究助手の楢原彩、支援企業の担当者・水沢浩司、園芸員の大西蒼太の三人です。" category: 植物園ミステリ difficulty: 2 estimatedMinutes: 10 - tags: [植物園, 朝, 作業記録] victim: name: 木島祥子 introduction: 市立緑苑植物園主任研究員 + foundAt: 06:30 + foundIn: 標本庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 木島祥子は標本庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「木島の希少植物管理ノート」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,92 +45,90 @@ facts: - id: narahara-sold-cuttings statement: 楢原彩は希少植物の挿し穂を無断で持ち出し、収集家へ販売していた kind: motive - secret: true - id: kijima-found-missing-tags statement: 木島祥子は事件前日、増殖記録と鉢の管理札の数が合わないことから楢原彩の持ち出しを疑った kind: motive - secret: true - id: kijima-confronted-narahara statement: 5時45分ごろ、木島祥子は楢原彩に持ち出しを問いただし、開園後に園長へ報告すると告げた kind: motive - secret: true - id: mizusawa-demanded-naming-rights statement: 水沢浩司は寄付継続の条件として新温室へ企業名を付けるよう木島祥子へ強く求め、拒否されていた kind: motive - secret: true - id: manual-water-zero statement: 5時50分から6時20分まで、東温室の手灌水用水道は使用量がゼロだった kind: physical - secret: true - id: narahara-left-east-house statement: 5時52分ごろ、楢原彩は東温室を離れた kind: truth - secret: true - id: onishi-saw-narahara-0605 statement: 6時05分ごろ、大西蒼太は標本庫へ向かう通路で楢原彩を見た kind: observation - id: narahara-killed-kijima-0608 statement: 6時08分ごろ、楢原彩は標本庫で木島祥子を襲い死亡させた kind: truth - secret: true - id: narahara-returned-east-0615 statement: 6時15分ごろ、楢原彩は東温室へ戻った kind: truth - secret: true - id: auto-misting-ran-0600 statement: 6時00分から6時04分まで、東温室では自動ミスト装置だけが予定どおり作動した kind: physical - id: onishi-falsified-overtime statement: 大西蒼太は前月の残業時間を実際より多く申請していた kind: other - secret: true - id: body-found-0630 statement: 6時30分、水沢浩司が標本庫で木島祥子の死を発見した kind: observation - id: missing-cuttings-list statement: 木島祥子のノートには、管理記録から消えた希少植物の挿し穂と楢原彩の担当日が一覧にされていた kind: physical - secret: true timeline: - id: confrontation at: "05:45" participants: [narahara] facts: [kijima-found-missing-tags, kijima-confronted-narahara] description: 木島が希少植物の持ち出しを楢原へ問いただし、開園後の報告を告げる。 + location: 植物園内 - id: narahara-leaves at: "05:52" participants: [narahara] facts: [narahara-left-east-house, manual-water-zero] + record: 水道メーター description: 楢原が東温室を離れる。手灌水用水道は使われていない。 + location: 東温室 - id: auto-misting at: "06:00" participants: [] facts: [auto-misting-ran-0600] + record: ミスト記録 description: 東温室で自動ミスト装置が予定どおり作動する。 + location: 東温室 - id: corridor-sighting at: "06:05" participants: [narahara, onishi] facts: [onishi-saw-narahara-0605] description: 大西が標本庫へ向かう通路で楢原を目撃する。 + location: 通路 - id: kijima-death at: "06:08" participants: [narahara] facts: [narahara-killed-kijima-0608] description: 楢原が標本庫で木島を襲い、木島は死亡する。 + location: 標本庫 - id: narahara-returns at: "06:15" participants: [narahara] facts: [narahara-returned-east-0615] description: 楢原が東温室へ戻る。 + location: 東温室 - id: discovery at: "06:30" participants: [mizusawa, narahara, onishi] facts: [body-found-0630] description: 水沢が標本庫で木島の死を発見する。 + location: 標本庫 characters: - id: narahara name: 楢原彩 - role: suspect publicIntroduction: "植物園の研究助手。" personality: 植物の扱いは繊細で、記録にも几帳面な研究助手。希少種への愛着は強いが、自分の持ち出しを「余った挿し穂の有効利用」と正当化している。作業手順を細かく話して信用を得ようとする。 goals: @@ -152,7 +157,6 @@ characters: strategy: maintain-until-contradicted memories: - id: missing-tags - about: kijima-confronted-narahara detail: 木島が欠けた管理札の一覧を見せ「開園したら園長に話す」と言ったとき、胸が締めつけられた。 relationships: - character: onishi @@ -160,7 +164,6 @@ characters: attitude: 設備には詳しいが口が軽いと思っている - id: mizusawa name: 水沢浩司 - role: suspect publicIntroduction: "丁寧な言葉を崩さない企業担当者。" personality: 丁寧な言葉を崩さない企業担当者。寄付を交渉材料に使ったことを表へ出したくなく、木島との関係を必要以上に穏便だったように話す。 goals: @@ -177,12 +180,10 @@ characters: strategy: maintain-until-contradicted memories: - id: naming-rights-rejection - about: mizusawa-demanded-naming-rights detail: 木島に「研究施設にスポンサー名を付けるつもりはありません」ときっぱり断られた場面を覚えている。 relationships: [] - id: onishi name: 大西蒼太 - role: witness publicIntroduction: "朝が早い仕事に慣れた園芸員。" personality: 朝が早い仕事に慣れた園芸員。設備の数値には正確だが、自分の残業申請の水増しが見つかるのを恐れている。人間関係の揉め事には首を突っ込みたくない。 goals: @@ -199,7 +200,6 @@ characters: strategy: maintain-until-contradicted memories: - id: corridor-narahara - about: onishi-saw-narahara-0605 detail: 6時05分ごろ、東温室にいるはずの楢原が標本庫の方へ歩いていくのを見て、作業変更かなと思った。 relationships: - character: narahara @@ -247,7 +247,6 @@ evidences: label: 東温室の手灌水用水道メーター description: 5時50分から6時20分まで手灌水の使用量はゼロ。6時から6時04分の自動ミストだけが別系統で作動している。 reveal: - mode: conversation condition: 楢原に手灌水の具体的な手順を尋ねるか、大西にその時間帯の水道使用量を確認したら開示する。 sources: - type: character @@ -260,7 +259,6 @@ evidences: label: 六時五分の標本庫通路の目撃 description: 大西は6時05分ごろ、標本庫へ向かう楢原を見ている。 reveal: - mode: conversation condition: 大西に6時前後に温室以外で誰を見たか尋ねたら開示する。 sources: - type: character @@ -271,20 +269,20 @@ evidences: label: 木島の希少植物管理ノート description: 消えた挿し穂と楢原の担当日が一覧化され、管理札の欠落も記録されている。 reveal: - mode: conversation - condition: 楢原に希少植物の管理札が足りない理由を尋ねるか、大西に木島が前日から調べていた記録を尋ねたら開示する。 + condition: 楢原に希少植物の管理札が足りない理由を尋ねるか、大西に木島が前日から調べていた記録を尋ねたら開示する。または遺体・現場を調べ、「木島の希少植物管理ノート」に関わる資料を確認したら開示する。 sources: - type: character id: narahara - type: character id: onishi + - type: victim + id: victim supports: [narahara-sold-cuttings, kijima-found-missing-tags, missing-cuttings-list] contradicts: [] - id: sponsor-email label: 命名権を条件にした寄付メール description: 水沢が寄付継続と新温室の企業名表示を結びつけて要求したメール。事件時刻の行動とは関係しない。 reveal: - mode: conversation condition: 水沢に寄付へ条件を付けていないか尋ね、圧力を否定したら開示する。 sources: - type: character @@ -295,7 +293,6 @@ evidences: label: 大西の残業申請と入退室記録 description: 申請時間の一部に施設内へいなかった記録があり、残業水増しは分かるが事件当朝の目撃とは両立する。 reveal: - mode: conversation condition: 大西に勤怠上の問題がないか尋ね、申請をごまかしていないと否定したら開示する。 sources: - type: character @@ -307,18 +304,9 @@ solution: summary: 犯人は楢原彩。希少植物の挿し穂を無断で販売していたことを木島に見抜かれ、開園後に園長へ報告すると告げられた。楢原は「5時50分から6時20分まで東温室で手灌水していた」と主張するが、その時間帯の手灌水用水道の使用量はゼロで、動いていたのは自動ミストだけだった。楢原は5時52分ごろ東温室を離れ、6時05分には大西が標本庫方向へ向かう姿を目撃している。6時08分ごろ木島を襲い、6時15分ごろ東温室へ戻った。水沢の寄付条件と大西の残業水増しは、それぞれ別の隠し事である。 method: 手灌水を続けていたという作業アリバイを作り、実際には東温室を離れて標本庫で木島を襲った後に戻った motive: 希少植物の無断持ち出しと販売が園長へ報告され、研究職を失うことを恐れたため - requiredFacts: [narahara-sold-cuttings, kijima-confronted-narahara, manual-water-zero, narahara-left-east-house, onishi-saw-narahara-0605, narahara-killed-kijima-0608, narahara-returned-east-0615] secretKeywords: - 犯人は楢原 - 楢原が犯人 - 楢原が木島を襲 - 私が木島を襲 - 手灌水をアリバイに殺 -quality: - expectedQuestionCount: - min: 8 - max: 17 - requiredEvidence: - min: 2 - redHerrings: [mizusawa-demanded-naming-rights, onishi-falsified-overtime] - notes: 核心は「作業をしていた」という証言を、その作業なら必ず残る水使用量で検証すること。大西の目撃を第二経路にして、設備ログだけに依存しない。 diff --git a/db/scenarios/ink-stained-contract.yaml b/db/scenarios/ink-stained-contract.yaml index d2fbbef..74ce02c 100644 --- a/db/scenarios/ink-stained-contract.yaml +++ b/db/scenarios/ink-stained-contract.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: ink-stained-contract meta: - title: 青燈社、深夜の編集部 + title: "原稿は人を殺さない" synopsis: "午後九時、出版社「青燈社」の編集部で、編集長の石橋礼司が個室内で死亡しているのが見つかりました。夜遅くまで残っていたのは、著作権エージェントの川瀬梨奈、編集者の藤本圭、作家の志堂透の三人です。" category: 出版社ミステリ difficulty: 3 estimatedMinutes: 10 - tags: [出版社, 契約書, 時刻] victim: name: 石橋礼司 introduction: 出版社「青燈社」編集長 + foundAt: 21:00 + foundIn: 編集長室 + estimatedDeathAt: "20:41" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 石橋礼司は編集長室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「海外版権の送金記録と支払帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,46 +46,36 @@ facts: - id: kawase-diverted-royalties statement: 川瀬梨奈は海外版権の入金の一部を作家へ報告せず、自分の管理口座へ留めていた kind: motive - secret: true - id: ishibashi-found-royalty-gap statement: 石橋礼司は事件当日、海外版権の送金記録と作家への支払額の差から川瀬梨奈の不正を見抜いた kind: motive - secret: true - id: ishibashi-warned-kawase statement: 20時25分ごろ、石橋礼司は川瀬梨奈に契約を打ち切り、翌朝作家と弁護士へ不正を知らせると告げた kind: motive - secret: true - id: shido-plagiarism statement: 志堂透の新作には、絶版小説から無断で流用した一節が含まれていた kind: other - secret: true - id: shido-argued-2031 statement: 20時31分から20時34分まで、志堂透は盗用疑惑を巡って石橋礼司と編集長室で口論した kind: observation - secret: true - id: kawase-entered-2038 statement: 20時38分ごろ、川瀬梨奈は編集長室へ入った kind: truth - secret: true - id: kawase-killed-ishibashi-2041 statement: 20時41分ごろ、川瀬梨奈は編集長室で石橋礼司を襲い死亡させた kind: truth - secret: true - id: fujimoto-saw-kawase-2044 statement: 20時44分ごろ、藤本圭は編集長室の前から離れる川瀬梨奈を見た kind: observation - id: contract-printed-2047 statement: 問題の契約書は20時47分、川瀬梨奈の端末から社内プリンターへ送信され印刷された kind: physical - secret: true - id: kawase-forged-signature statement: 川瀬梨奈は別の契約書にあった石橋礼司の署名を利用し、20時47分に印刷した書類へ石橋が署名したように偽装した kind: truth - secret: true - id: fujimoto-leaked-manuscript statement: 藤本圭は話題作りのため、未発表原稿の一部を知人の書評ブログへ匿名で流していた kind: other - secret: true - id: body-found-2100 statement: 21時00分、藤本圭が編集長室で石橋礼司の死を発見した kind: observation @@ -90,45 +88,53 @@ timeline: participants: [kawase] facts: [kawase-diverted-royalties, ishibashi-found-royalty-gap, ishibashi-warned-kawase] description: 石橋が川瀬の版権収入の不正を指摘し、翌朝の契約打ち切りと報告を告げる。 + location: 編集部内 - id: shido-argument at: "20:31" participants: [shido] facts: [shido-argued-2031, shido-plagiarism] description: 志堂が盗用疑惑を巡って石橋と口論する。 + location: 編集部内 - id: kawase-enters at: "20:38" participants: [kawase] facts: [kawase-entered-2038] description: 川瀬が編集長室へ入る。 + location: 編集長室 - id: ishibashi-death at: "20:41" participants: [kawase] facts: [kawase-killed-ishibashi-2041] description: 川瀬が石橋を襲い、石橋は編集長室で死亡する。 + location: 編集長室 - id: kawase-leaves at: "20:44" participants: [kawase, fujimoto] facts: [fujimoto-saw-kawase-2044] description: 藤本が編集長室前から離れる川瀬を目撃する。 + location: 編集長室前 - id: contract-print at: "20:47" participants: [kawase] facts: [contract-printed-2047, kawase-forged-signature] + record: 印刷履歴 description: 川瀬が契約書を印刷し、石橋が後から署名したように見える書類を作る。 + location: 編集部 - id: claimed-signing at: "20:50" participants: [kawase] facts: [signed-contract-claims-2050] description: 川瀬は後に、この時刻に石橋本人から署名をもらったと説明する。 + location: 編集部内 - id: discovery at: "21:00" participants: [fujimoto, kawase, shido] facts: [body-found-2100] description: 藤本が編集長室で石橋の死を発見する。 + location: 編集長室 characters: - id: kawase name: 川瀬梨奈 - role: suspect publicIntroduction: "交渉上手で感情を表に出さない著作権エージェント。" personality: 交渉上手で感情を表に出さない著作権エージェント。契約書や入金記録の細部まで把握しているが、自分の管理する口座について尋ねられると話を一般論へそらす。石橋とは長く仕事をしてきた。 goals: @@ -157,7 +163,6 @@ characters: strategy: maintain-until-contradicted memories: - id: contract-termination - about: ishibashi-warned-kawase detail: 石橋から「明日の朝、作家にも弁護士にも全部見せる」と言われたとき、今まで築いた仕事が一気に崩れると思った。 relationships: - character: fujimoto @@ -165,7 +170,6 @@ characters: attitude: 真面目だが融通が利かないと思っている - id: fujimoto name: 藤本圭 - role: witness publicIntroduction: "几帳面で締切に厳しい編集者。" personality: 几帳面で締切に厳しい編集者。話題作りのため未発表原稿を流したことを強く後悔しており、端末ログを詳しく調べられるのを嫌がる。人の出入りにはよく気づく。 goals: @@ -182,15 +186,15 @@ characters: strategy: maintain-until-contradicted memories: - id: kawase-at-door - about: fujimoto-saw-kawase-2044 detail: 20時44分ごろ、編集長室の扉を静かに閉めて離れる川瀬を見た。声をかけたが返事が短かった。 + - id: death-estimate-memory + detail: 発見時の石橋の状態と編集長室の室温を確認しており、死亡は20時41分ごろと見積もられるという確認内容を覚えている。 relationships: - character: kawase relation: 仕事上の取引相手 attitude: 契約の話になると隙がない人だと思っている - id: shido name: 志堂透 - role: suspect publicIntroduction: "評判を気にする中堅作家。" personality: 評判を気にする中堅作家。自尊心が高く、盗用疑惑を認めるくらいなら石橋との口論自体を隠したいと思っている。感情的にはなるが、事件後は露骨に怯えている。 goals: @@ -209,7 +213,6 @@ characters: strategy: maintain-until-contradicted memories: - id: plagiarism-argument - about: shido-argued-2031 detail: 石橋に原稿の一節を示され「これ、元の作品を知ってるよ」と言われ、思わず声を荒らげたことを覚えている。 relationships: [] revelations: @@ -254,7 +257,6 @@ evidences: label: 社内プリンターの二十時四十七分の履歴 description: 問題の契約書は20時47分に川瀬の端末から送信され、その時刻に初めて印刷されている。 reveal: - mode: conversation condition: 川瀬に契約書を用意した時刻を尋ねるか、藤本に社内プリンターの履歴を確認できないか尋ねたら開示する。 sources: - type: character @@ -267,7 +269,6 @@ evidences: label: 二十時四十四分の編集長室前の目撃 description: 藤本は20時44分ごろ、編集長室から離れる川瀬を見ている。 reveal: - mode: conversation condition: 藤本に20時40分台の編集長室前で誰を見たか尋ねたら開示する。 sources: - type: character @@ -278,20 +279,20 @@ evidences: label: 海外版権の送金記録と支払帳簿 description: 海外出版社からの入金額に対して作家へ報告された金額が少なく、川瀬の管理口座へ差額が残っている。 reveal: - mode: conversation - condition: 川瀬に石橋が調べていた版権収入について尋ねるか、藤本に石橋が事件前に確認していた帳簿を尋ねたら開示する。 + condition: 川瀬に石橋が調べていた版権収入について尋ねるか、藤本に石橋が事件前に確認していた帳簿を尋ねたら開示する。または遺体・現場を調べ、「海外版権の送金記録と支払帳簿」に関わる資料を確認したら開示する。 sources: - type: character id: kawase - type: character id: fujimoto + - type: victim + id: victim supports: [kawase-diverted-royalties, ishibashi-found-royalty-gap, ishibashi-warned-kawase] contradicts: [] - id: plagiarism-comparison label: 志堂の原稿と絶版小説の比較 description: 複数の表現が一致しており、志堂が石橋と盗用疑惑で揉めていた理由は分かるが、20時34分以降の行動とは結びつかない。 reveal: - mode: conversation condition: 志堂に石橋と原稿について揉めていなかったか尋ね、口論を否定したら開示する。 sources: - type: character @@ -302,30 +303,31 @@ evidences: label: 未発表原稿の送信履歴 description: 藤本の端末から社外へ未発表原稿の一部が送られた記録が残るが、石橋の死亡とは無関係である。 reveal: - mode: conversation condition: 藤本に原稿漏洩の可能性を尋ね、社外送信を否定したため端末履歴を確認したら開示する。 sources: - type: character id: fujimoto supports: [fujimoto-leaked-manuscript] contradicts: ["lie:fujimoto-no-leak"] + - id: death-estimate + label: 編集長室の死亡推定 + description: 編集長室の室温と発見時の状態を合わせると、石橋の死亡は20時41分ごろと見積もられる。20時50分の署名時刻より前である。 + reveal: + condition: 遺体を調べて発見時の状態から死亡時刻を推定するか、藤本に発見時に確認した状態と時刻について尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: fujimoto } + supports: [kawase-killed-ishibashi-2041] + contradicts: [] + revealsDeathTime: true solution: culprit: kawase summary: 犯人は川瀬梨奈。海外版権収入の一部を留保していた不正を石橋に見抜かれ、翌朝に契約を打ち切って作家と弁護士へ知らせると告げられた。川瀬は20時38分ごろ編集長室へ入り、20時41分ごろ石橋を襲った。20時44分には藤本が編集長室前から離れる川瀬を見ている。その後20時47分、川瀬は新しい契約書を印刷し、石橋が20時50分に署名したように見える書類を作った。しかしプリンター記録はその紙が20時47分に初めて作られたことを示し、署名を生存証明として使う川瀬の説明を崩す。志堂の盗用と藤本の原稿漏洩は独立した秘密である。 method: 石橋を編集長室で襲った後、20時47分に契約書を作成して既存の署名を利用し、20時50分まで石橋が生きていたように偽装した motive: 海外版権収入の不正が発覚し、契約打ち切りと作家・弁護士への報告で仕事を失うことを恐れたため - requiredFacts: [kawase-diverted-royalties, ishibashi-warned-kawase, kawase-entered-2038, kawase-killed-ishibashi-2041, fujimoto-saw-kawase-2044, contract-printed-2047, kawase-forged-signature] secretKeywords: - 犯人は川瀬 - 川瀬が犯人 - 川瀬が石橋を襲 - 私が石橋を襲 - 契約書を偽造して生存を偽装 -quality: - expectedQuestionCount: - min: 8 - max: 18 - requiredEvidence: - min: 2 - redHerrings: [shido-plagiarism, fujimoto-leaked-manuscript] - notes: 書類に署名があることを生存証明とみなす思い込みを、印刷時刻で崩す。藤本の20時44分の目撃を合わせれば川瀬が死亡直後に編集長室から出たことまで繋がる。 diff --git a/db/scenarios/landslide-clock-museum-eleven-minutes.yaml b/db/scenarios/landslide-clock-museum-eleven-minutes.yaml index 249f8ef..114946e 100644 --- a/db/scenarios/landslide-clock-museum-eleven-minutes.yaml +++ b/db/scenarios/landslide-clock-museum-eleven-minutes.yaml @@ -1,15 +1,34 @@ schemaVersion: 1 id: landslide-clock-museum-eleven-minutes meta: - title: 崩落の時計博物館、閉館後の夜 + title: "時計博物館に朝は遠い" synopsis: "午後八時四十二分、山間の私設時計博物館の修復室で、学芸員長の倉橋宗一が死亡しているのが見つかりました。午後七時すぎの土砂崩れで唯一の山道は通行不能となり、館内には倉橋を含め五人しか残っていませんでした。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [時計博物館, 崩落, 時刻錯誤, 最後の目撃] victim: name: 倉橋宗一 introduction: 私設時計博物館学芸員長 + foundAt: 20:42 + foundIn: 修復室 + estimatedDeathAt: "20:28" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 倉橋宗一は修復室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「倉橋の来歴照合メモ」に関わる資料が残されている。 +places: + - id: master-clock + name: 親時計盤 + shortName: 親時計 + introduction: 館内の展示時計と時報へ基準時刻を配る同期盤 + situation: 保守扉に夕方の同期試験票が挟まれたままになっている + findings: + - id: master-clock-offset + statement: 親時計盤の補正値は正しい時刻より十一分進む設定になっている。 + - id: independent-security-clock + statement: 防災端末の時刻は親時計盤と別系統で、同じ瞬間を十一分早い数字で記録している。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +60,12 @@ facts: - id: shiba-faked-provenance statement: 志波沙月は修復実績を上げるため、由来が不確かな時計を著名工房の作品として展示記録に登録していた kind: motive - secret: true - id: kurahashi-found-provenance-fraud statement: 倉橋宗一は事件当日、志波沙月が展示時計の来歴を偽っていたことに気づいた kind: motive - secret: true - id: kurahashi-planned-withdrawal statement: 倉橋宗一は翌朝、問題の時計を展示から外し、志波沙月の記録改ざんを理事会へ報告する予定だった kind: motive - secret: true - id: master-clock-fast-eleven statement: 事件当夜、中央ホールの親時計は正しい時刻より十一分進んでいた kind: physical @@ -68,30 +84,24 @@ facts: - id: shiba-left-west-corridor statement: 20時22分ごろ、志波沙月は西回廊を離れて修復室側へ向かった kind: truth - secret: true - id: asakura-saw-shiba-2024 statement: 20時24分ごろ、朝倉真紀は修復室へ続く北廊下で志波沙月を見た kind: observation - id: shiba-killed-kurahashi statement: 20時28分ごろ、志波沙月は修復室で倉橋宗一を襲い死亡させた kind: truth - secret: true - id: shiba-returned-gallery statement: 20時34分ごろ、志波沙月は展示準備室へ戻った kind: truth - secret: true - id: hoshina-sold-catalog-images statement: 保科悠人は未公開収蔵品の写真を許可なく外部の収集家へ送っていた kind: other - secret: true - id: asakura-hid-cash-error statement: 朝倉真紀は当日の売上金の不足を自分の集計ミスだと知りながら報告を先延ばしにしていた kind: other - secret: true - id: kido-caused-clock-offset statement: 城戸篤は夕方の同期試験で親時計の補正値を誤り、十一分進んだ状態のまま復旧完了とした kind: other - secret: true - id: body-found-2042 statement: 20時42分、保科悠人が修復室で倉橋宗一の死を発見した kind: observation @@ -101,47 +111,56 @@ timeline: at: "19:36" participants: [kido] facts: [master-clock-fast-eleven, gallery-clocks-follow-master, kido-caused-clock-offset] + record: 同期試験票 description: 同期試験後、親時計と展示時計が正しい時刻より十一分進んだ状態で残る。 + location: 館内 - id: false-half-past-chime at: "20:19" participants: [shiba, asakura, hoshina] facts: [half-past-chime-actual-2019, security-clock-accurate] + record: 防災端末記録 description: 館内時計が20時30分を示し、大時計が半時の鐘を鳴らす。実際の時刻は20時19分だった。 + location: 館内 - id: shiba-kurahashi-talk at: "20:20" participants: [shiba] facts: [shiba-spoke-kurahashi-2020] description: 志波が西回廊で倉橋と短く話す。 + location: 西回廊 - id: shiba-leaves-west at: "20:22" participants: [shiba] facts: [shiba-left-west-corridor] description: 志波が西回廊を離れ、修復室側へ向かう。 + location: 西回廊 - id: asakura-sighting at: "20:24" participants: [shiba, asakura] facts: [asakura-saw-shiba-2024] description: 朝倉が北廊下で志波を目撃する。 + location: 北廊下 - id: kurahashi-death at: "20:28" participants: [shiba] facts: [shiba-killed-kurahashi] description: 志波が修復室で倉橋を襲い、倉橋は死亡する。 + location: 修復室 - id: shiba-return at: "20:34" participants: [shiba] facts: [shiba-returned-gallery] description: 志波が展示準備室へ戻る。 + location: 展示準備室 - id: discovery at: "20:42" participants: [hoshina, shiba, asakura, kido] facts: [body-found-2042] description: 保科が修復室で倉橋の死を発見する。 + location: 修復室 characters: - id: shiba name: 志波沙月 - role: suspect publicIntroduction: "古時計の修復技師。" personality: 古時計への知識と誇りが強い修復技師。細部をよく覚えているように話すが、時刻については館内の鐘を意図的に基準にする。倉橋に記録改ざんを見抜かれたことを隠したい。 goals: @@ -172,7 +191,6 @@ characters: strategy: maintain-until-contradicted memories: - id: board-report-threat - about: kurahashi-planned-withdrawal detail: 倉橋に「明日、時計を展示から外して理事会にも記録を出す」と言われ、修復技師としての評価が終わると思った。 relationships: - character: kido @@ -183,7 +201,6 @@ characters: attitude: 収蔵品を軽く扱うところが嫌い - id: asakura name: 朝倉真紀 - role: witness publicIntroduction: "事務処理に強い受付責任者。" personality: 事務処理に強い受付責任者。売上金の集計ミスを隠したいので閉館後の帳簿作業を曖昧にするが、人とすれ違った順番はよく覚えている。時刻は大時計の鐘を信用していた。 goals: @@ -200,7 +217,6 @@ characters: strategy: maintain-until-contradicted memories: - id: five-minutes-after-bell - about: asakura-saw-shiba-2024 detail: 半時の鐘が鳴ってから五分ほど後、北廊下で修復室の方から来る志波を見た。館内時計では20時35分近くだと思っていた。 relationships: - character: kido @@ -208,7 +224,6 @@ characters: attitude: 夕方から時計の同期試験でもたついていたのを覚えている - id: hoshina name: 保科悠人 - role: suspect publicIntroduction: "研究熱心だが承認欲求の強い若手学芸員。" personality: 研究熱心だが承認欲求の強い若手学芸員。未公開写真を外部へ送ったことが露見するのを恐れ、収蔵庫周辺の行動を隠す。倉橋とは展示方針で衝突していた。 goals: @@ -225,15 +240,15 @@ characters: strategy: maintain-until-contradicted memories: - id: master-clock-looked-odd - about: master-clock-fast-eleven detail: 20時台に自分の腕時計と中央ホールの時計を見比べ、館内時計の方がかなり進んでいる気がしたが、作業中で深く考えなかった。 + - id: death-estimate-memory + detail: 発見時の確認内容を防災端末の正しい時刻で控えており、倉橋の死亡は20時28分ごろと見積もられると覚えている。 relationships: - character: shiba relation: 同僚 attitude: 技術は尊敬しているが、作品の価値を自分の手柄として語りすぎると思っている - id: kido name: 城戸篤 - role: suspect publicIntroduction: "時計博物館の設備技術者。" personality: 機器の仕組みを平易に説明できる設備技術者。夕方の同期試験ミスを隠したいので最初は時計の精度に問題はなかったと言い張るが、防災端末と親時計が別系統であることは知っている。 goals: @@ -250,7 +265,6 @@ characters: strategy: maintain-until-contradicted memories: - id: offset-screen - about: master-clock-fast-eleven detail: 同期試験の最後に補正値を一桁見間違えたかもしれないと気づいたが、崩落対応で呼ばれて確認を後回しにした。 relationships: [] @@ -318,18 +332,17 @@ evidences: label: 親時計と防災端末の時刻差 description: 防災端末が20時19分を記録した瞬間の監視画像で、中央ホールの親時計は20時30分を示している。両系統には十一分の差がある。 reveal: - mode: conversation - condition: 城戸か保科に館内時計の精度と、防災端末など別系統の時計との比較を尋ねたら開示する。 + condition: 城戸か保科に館内時計の精度と、防災端末など別系統の時計との比較を尋ねたら開示する。または親時計盤を調べ、防災端末と表示時刻を突き合わせたら開示する。 sources: - { type: character, id: kido } - { type: character, id: hoshina } + - { type: location, id: master-clock } supports: [master-clock-fast-eleven, gallery-clocks-follow-master, security-clock-accurate, half-past-chime-actual-2019] contradicts: ["lie:kido-clock-accurate", "lie:shiba-late-last-seen"] - id: north-corridor-sighting label: 半時の鐘から五分後の志波 description: 朝倉は半時の鐘から約五分後、修復室へ続く北廊下で志波を見ている。鐘の実時刻を補正すると20時24分ごろになる。 reveal: - mode: conversation condition: 朝倉に志波を見た時刻を時計の数字ではなく、鐘からの経過時間も含めて尋ねたら開示する。 sources: - { type: character, id: asakura } @@ -339,18 +352,17 @@ evidences: label: 倉橋の来歴照合メモ description: 志波が登録した著名工房の来歴と原資料が一致せず、倉橋が翌朝の展示撤去と理事会報告を予定していたことが分かる。 reveal: - mode: conversation - condition: 志波か保科に倉橋が事件直前に調べていた展示時計の来歴について尋ね、登録内容の不一致を追及したら開示する。 + condition: 志波か保科に倉橋が事件直前に調べていた展示時計の来歴について尋ね、登録内容の不一致を追及したら開示する。または遺体・現場を調べ、「倉橋の来歴照合メモ」に関わる資料を確認したら開示する。 sources: - { type: character, id: shiba } - { type: character, id: hoshina } + - { type: victim, id: victim } supports: [shiba-faked-provenance, kurahashi-found-provenance-fraud, kurahashi-planned-withdrawal] contradicts: [] - id: hoshina-image-history label: 保科の画像送信履歴 description: 未公開収蔵品の画像が保科の端末から外部へ送られていたことが分かるが、修復室の事件とは独立している。 reveal: - mode: conversation condition: 保科に未公開画像を外部へ送っていないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: hoshina } @@ -360,7 +372,6 @@ evidences: label: 朝倉の売上金再集計表 description: 売上金不足は朝倉自身の二重計上によるものと分かるが、倉橋の死亡とは関係がない。 reveal: - mode: conversation condition: 朝倉に売上金不足の原因を尋ね、集計ミスではないという説明を検証したら開示する。 sources: - { type: character, id: asakura } @@ -370,30 +381,31 @@ evidences: label: 城戸の同期試験作業票 description: 親時計の補正値を誤った可能性を示す途中メモが残っており、城戸が時刻ずれを把握しながら確認を後回しにしたことが分かる。 reveal: - mode: conversation condition: 城戸に夕方の同期試験で補正値を誤っていないか尋ね、作業票と説明を照合したら開示する。 sources: - { type: character, id: kido } supports: [kido-caused-clock-offset, master-clock-fast-eleven] contradicts: ["lie:kido-clock-accurate"] + - id: death-estimate + label: 修復室の死亡推定 + description: 発見時の状態を、親時計とは別系統の防災端末時刻で整理すると、倉橋の死亡は20時28分ごろと見積もられる。 + reveal: + condition: 遺体を調べて発見時の状態を正しい時刻系で確認するか、保科に発見時の確認内容を防災端末の時刻基準で尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: hoshina } + supports: [shiba-killed-kurahashi] + contradicts: [] + revealsDeathTime: true solution: culprit: shiba summary: 犯人は志波沙月。展示時計の来歴を偽ったことを倉橋に見抜かれ、翌朝に展示を外され理事会へ報告される予定だった。館内の親時計は十一分進んでおり、20時30分の鐘が鳴った実時刻は20時19分だった。志波が倉橋と話したのはその直後の20時20分ごろで、志波の「20時31分ごろにも倉橋は生きていた」という説明は十一分ずれている。志波は20時22分ごろ西回廊を離れ、20時24分には朝倉が修復室側の北廊下で志波を目撃している。20時28分ごろ倉橋を襲った後、20時34分ごろ展示準備室へ戻った。館内時計の表示を正しい時刻だと思い込むと、最後の目撃が十一分後ろへずれて犯行時間そのものを誤る仕掛けだった。 method: 十一分進んだ館内時計と時報を基準に最後の目撃時刻を遅く見せ、実際にはその後の空白時間に修復室へ移動して倉橋を襲った motive: 展示時計の来歴改ざんが発覚し、翌朝の展示撤去と理事会報告で修復技師としての信用を失うことを恐れたため - requiredFacts: [shiba-faked-provenance, kurahashi-planned-withdrawal, master-clock-fast-eleven, security-clock-accurate, half-past-chime-actual-2019, shiba-spoke-kurahashi-2020, shiba-left-west-corridor, asakura-saw-shiba-2024, shiba-killed-kurahashi] secretKeywords: - 犯人は志波 - 志波が犯人 - 志波が倉橋を襲 - 私が倉橋を襲 - 十一分ずれで犯行時刻を偽装 -quality: - expectedQuestionCount: - min: 14 - max: 28 - requiredEvidence: - min: 3 - redHerrings: [hoshina-sold-catalog-images, asakura-hid-cash-error, kido-caused-clock-offset] - notes: 数字の時刻を直接聞くだけでは全員が同じ誤った時計を参照するため混乱する。鐘からの相対時間と別系統の防災端末を組み合わせて十一分を補正し、志波の最後の目撃と朝倉の目撃を実時刻へ戻すのが主経路。 diff --git a/db/scenarios/landslide-hotel-frosted-silhouette.yaml b/db/scenarios/landslide-hotel-frosted-silhouette.yaml index 15a0f0c..fc4b9ea 100644 --- a/db/scenarios/landslide-hotel-frosted-silhouette.yaml +++ b/db/scenarios/landslide-hotel-frosted-silhouette.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: landslide-hotel-frosted-silhouette meta: - title: 崖上ホテル、孤立の夜 + title: "崖の上にホテルがひとつ" synopsis: "午後十時、山腹の小さなホテルで支配人・長峰宗一が執務室内で死亡しているのが見つかりました。夕方からの豪雨で道路は土砂崩れに塞がれ、ホテルには外部から出入りできません。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [ホテル, 土砂崩れ, 目撃, 死亡時刻] victim: name: 長峰宗一 introduction: 山腹のホテル支配人 + foundAt: 22:00 + foundIn: 執務室 + estimatedDeathAt: "21:05" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 長峰宗一は執務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「水増しされた改装請求書」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,27 +46,21 @@ facts: - id: renovation-shortage statement: 長峰宗一は事件当日の夕方、改装費の請求書に不自然な水増しを見つけた kind: motive - secret: true - id: ayase-falsified-invoices statement: 綾瀬環は改装業者への請求額を水増しし、差額を私的に流用していた kind: motive - secret: true - id: nagamine-called-ayase statement: 20時52分ごろ、長峰宗一は綾瀬環を執務室に呼び、請求書について説明を求めた kind: motive - secret: true - id: ayase-killed-nagamine statement: 21時05分ごろ、綾瀬環は執務室で長峰宗一を襲い死亡させた kind: truth - secret: true - id: ayase-staged-silhouette statement: 21時23分ごろ、綾瀬環は長峰宗一の長い上着を背の高い衣紋掛けに掛け、執務室の磨りガラス越しに人影へ見えるよう配置した kind: truth - secret: true - id: desk-lamp-left-on statement: 綾瀬環は執務室内の机上灯をつけ、人影の輪郭が廊下側へ映る状態にした kind: truth - secret: true - id: kanda-saw-silhouette statement: 21時30分ごろ、神田理一は磨りガラス越しに長峰宗一の長い上着に似た輪郭の人影を見た kind: observation @@ -77,11 +79,9 @@ facts: - id: kanda-secret-manuscript statement: 神田理一は長峰宗一から預かった未発表の回想録を無断で複写していた kind: other - secret: true - id: hoshino-secret-photo statement: 星野結は契約外の館内写真を出版社へ売ろうとしていた kind: other - secret: true - id: body-found-2200 statement: 22時00分、星野結が執務室で長峰宗一の死を発見した kind: observation @@ -91,35 +91,42 @@ timeline: participants: [ayase] facts: [renovation-shortage, ayase-falsified-invoices, nagamine-called-ayase] description: 長峰が綾瀬を執務室へ呼び、改装費の不自然な請求について説明を求める。 + location: 執務室 - id: nagamine-death at: "21:05" participants: [ayase] facts: [ayase-killed-nagamine] description: 綾瀬が執務室で長峰を襲う。 + location: 執務室 - id: coat-missing at: "21:18" participants: [hoshino] facts: [hoshino-saw-empty-coat-hook] description: 星野がフロント裏の衣類掛けから長峰の長い上着がなくなっていることに気づく。 + location: フロント裏 - id: silhouette-staged at: "21:23" participants: [ayase] facts: [ayase-staged-silhouette, desk-lamp-left-on, stand-feet-dust-mark] + record: 床の移動跡 description: 綾瀬が上着と衣紋掛け、机上灯を使って磨りガラス越しの人影を作る。 + location: ホテル内 - id: silhouette-seen at: "21:30" participants: [kanda] facts: [kanda-saw-silhouette, kanda-did-not-hear-voice] description: 神田が廊下から人影を見て、長峰本人が執務室にいると思い込む。 + location: 執務室 - id: discovery at: "22:00" participants: [hoshino, ayase, kanda] facts: [body-found-2200, coat-returned-after-discovery] + record: 上着 description: 星野が執務室で長峰の死を発見し、室内の衣紋掛けには長峰の上着が残されている。 + location: 執務室 characters: - id: ayase name: 綾瀬環 - role: suspect publicIntroduction: "愛想がよく館内の細かな要望にもすぐ対応するフロント主任。" personality: 愛想がよく館内の細かな要望にもすぐ対応するフロント主任。備品の位置や客の動線を熟知している。請求書の話になると説明を曖昧にし、21時半の人影を長峰本人だったと強く主張する。 goals: @@ -148,7 +155,6 @@ characters: strategy: maintain-until-contradicted memories: - id: frosted-glass-idea - about: ayase-staged-silhouette detail: 雨の日は執務室の磨りガラスに中の輪郭だけが濃く映ることを、毎晩フロントから見て知っていた。 relationships: - character: kanda @@ -159,7 +165,6 @@ characters: attitude: 館内を細かく撮影しているため、余計な物の位置まで覚えていそうで警戒している - id: kanda name: 神田理一 - role: witness publicIntroduction: "ホテルに宿泊していた作家。" personality: 観察したことを文章にする癖がある作家だが、一度意味づけした光景を事実そのものと思い込みやすい。人影を見たことには自信があるが、顔も声も確認していない。 goals: @@ -176,7 +181,6 @@ characters: strategy: maintain-until-contradicted memories: - id: long-coat-shadow - about: kanda-saw-silhouette detail: 磨りガラスの向こうに肩から下へ長く落ちる輪郭があり、長峰のいつもの上着だと思った。それだけで本人だと決めつけた。 relationships: - character: ayase @@ -187,7 +191,6 @@ characters: attitude: 写真で物の位置をよく覚えているので観察力は信用している - id: hoshino name: 星野結 - role: suspect publicIntroduction: "光や構図に敏感な写真家。" personality: 光や構図に敏感な写真家。物の位置が変わるとすぐ気づく。契約外の写真を売ろうとした後ろめたさがあり、その話題では歯切れが悪い。 goals: @@ -204,8 +207,9 @@ characters: strategy: maintain-until-contradicted memories: - id: missing-coat - about: hoshino-saw-empty-coat-hook detail: フロント裏を撮る許可を確認したとき、いつも端に掛かっていた長峰の長い上着だけが消えていたので妙に目についた。 + - id: death-estimate-memory + detail: 最初に執務室へ入ったときの状態を覚えており、確認では長峰の死亡は21時05分ごろと見積もられていた。 relationships: - character: ayase relation: 撮影窓口 @@ -261,7 +265,6 @@ evidences: label: 神田の人影証言の詳細 description: 神田が確認したのは磨りガラス越しの長い輪郭だけで、顔も声も確認していない。 reveal: - mode: conversation condition: 神田に21時30分の目撃を具体的に描写してもらい、何を直接確認したのか問い直したら開示する。 sources: - type: character @@ -272,7 +275,6 @@ evidences: label: 長峰の上着の移動 description: 21時18分にはフロント裏から消えていた長峰の長い上着が、発見時には執務室の衣紋掛けに掛かっていた。 reveal: - mode: conversation condition: 星野にフロント裏で気づいた変化か、発見時の執務室内の上着について尋ねたら開示する。 sources: - type: character @@ -285,7 +287,6 @@ evidences: label: 衣紋掛けの移動跡 description: 執務室の床には衣紋掛けを窓際から磨りガラスの近くへ動かした跡が残る。 reveal: - mode: conversation condition: 星野に執務室内の物の位置を尋ねるか、綾瀬に衣紋掛けを動かした理由を確認したら開示する。 sources: - type: character @@ -298,20 +299,20 @@ evidences: label: 水増しされた改装請求書 description: 長峰が印を付けた請求書には、綾瀬が処理した項目に説明できない差額がまとまっている。 reveal: - mode: conversation - condition: 綾瀬に改装費の処理を尋ねるか、長峰が直前まで確認していた書類について追及したら開示する。 + condition: 綾瀬に改装費の処理を尋ねるか、長峰が直前まで確認していた書類について追及したら開示する。または遺体・現場を調べ、「水増しされた改装請求書」に関わる資料を確認したら開示する。 sources: - type: character id: ayase - type: character id: hoshino + - type: victim + id: victim supports: [renovation-shortage, ayase-falsified-invoices, nagamine-called-ayase] contradicts: ["lie:ayase-clean-invoices"] - id: manuscript-copy label: 無断複写された回想録 description: 神田の鞄から長峰の未発表回想録の複写が見つかるが、死亡時刻の偽装とは関係しない。 reveal: - mode: conversation condition: 神田に長峰から預かった原稿の扱いを尋ね、複写を否定したら開示する。 sources: - type: character @@ -322,29 +323,30 @@ evidences: label: 契約外写真の送付準備 description: 星野が契約外の館内写真を出版社へ送る準備をしていた記録。事件の人影偽装とは独立した秘密である。 reveal: - mode: conversation condition: 星野に契約外の写真利用について尋ね、売却予定を否定したら開示する。 sources: - type: character id: hoshino supports: [hoshino-secret-photo] contradicts: ["lie:hoshino-no-side-sale"] + - id: death-estimate + label: 執務室の死亡推定 + description: 執務室の室温と発見時の状態から、長峰の死亡は21時05分ごろと見積もられる。21時30分に見えた人影より前である。 + reveal: + condition: 遺体を調べて発見時の状態を確認するか、星野に最初に執務室へ入ったときの状態と確認内容を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: hoshino } + supports: [ayase-killed-nagamine] + contradicts: [] + revealsDeathTime: true solution: culprit: ayase summary: 犯人は綾瀬環。改装費の水増しを長峰に見抜かれ、説明を求められた直後に執務室で長峰を襲った。犯行を21時半以後に見せるため、長峰の長い上着を衣紋掛けに掛け、机上灯で磨りガラスへ人の輪郭を映した。神田はその輪郭を長峰本人だと思い込んだが、顔も声も確認していない。上着が21時18分にはフロント裏から消えていたこと、発見時には執務室の衣紋掛けに掛かっていたこと、衣紋掛けを磨りガラス付近へ移動した跡が残ることを合わせると、目撃は生存証明ではなく偽装だったと分かる。 method: 長峰を襲った後、本人の長い上着と衣紋掛け、室内灯で磨りガラス越しの人影を作り、死亡時刻を遅く見せた motive: 改装費の水増しと私的流用を長峰に発見され、責任追及を恐れたため - requiredFacts: [ayase-falsified-invoices, nagamine-called-ayase, ayase-killed-nagamine, ayase-staged-silhouette, kanda-did-not-hear-voice, hoshino-saw-empty-coat-hook, stand-feet-dust-mark] secretKeywords: - 犯人は綾瀬 - 綾瀬が長峰を襲 - 上着で人影を作 - 人影で死亡時刻を偽装 -quality: - expectedQuestionCount: - min: 10 - max: 22 - requiredEvidence: - min: 3 - redHerrings: [kanda-secret-manuscript, hoshino-secret-photo] - notes: 目撃証言を「見た事実」と「証人の解釈」に分けるのが中心。人影の正体だけでは犯人確定にせず、上着の移動と請求書の動機を合わせる。 diff --git a/db/scenarios/last-train-delay-certificate.yaml b/db/scenarios/last-train-delay-certificate.yaml index 2ca4703..25b22f2 100644 --- a/db/scenarios/last-train-delay-certificate.yaml +++ b/db/scenarios/last-train-delay-certificate.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: last-train-delay-certificate meta: - title: 夕凪駅、終電後の事件 + title: "終電のあとに駅は残る" synopsis: "午後十時二十五分、郊外駅「夕凪駅」の駅務室で、駅長の藤崎正雄が死亡しているのが見つかりました。その夜は信号トラブルで終電が八分遅れ、ホームには普段より長く乗客が残っていました。" category: 駅ミステリ difficulty: 3 estimatedMinutes: 10 - tags: [駅, 終電, 運行記録] victim: name: 藤崎正雄 introduction: 夕凪駅駅長 + foundAt: 22:25 + foundIn: 駅務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 藤崎正雄は駅務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「券売機返金処理と現金残高の差」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,48 +45,39 @@ facts: - id: arima-skimmed-refunds statement: 有馬結衣は自動券売機の返金処理を利用し、少額の現金を複数回抜き取っていた kind: motive - secret: true - id: fujisaki-found-refund-gap statement: 藤崎正雄は事件当日、返金処理記録と現金残高の差から有馬結衣の不正に気づいた kind: motive - secret: true - id: fujisaki-warned-arima statement: 21時55分ごろ、藤崎正雄は有馬結衣に、終電後すぐ本部へ不正を報告すると告げた kind: motive - secret: true - id: last-train-delayed statement: 本来22時10分発の終電は信号トラブルのため遅れ、実際には22時18分に夕凪駅を出発した kind: physical - id: arima-entered-office-2205 statement: 22時05分ごろ、有馬結衣はホームではなく駅務室へ続く通路に入った kind: truth - secret: true - id: sawada-saw-arima-2205 statement: 22時05分ごろ、沢田亮は駅務室へ向かう有馬結衣を見た kind: observation - id: arima-killed-fujisaki-2208 statement: 22時08分ごろ、有馬結衣は駅務室で藤崎正雄を襲い死亡させた kind: truth - secret: true - id: arima-left-office-2212 statement: 22時12分ごろ、有馬結衣は駅務室側の通路からホーム方向へ戻った kind: truth - secret: true - id: komori-saw-arima-2212 statement: 22時12分ごろ、小森拓は売店前から、駅務室側の通路から戻る有馬結衣を見た kind: observation - id: certificates-printed-2220 statement: 遅延証明書は終電出発後の22時20分に駅務室の専用プリンターから一括印刷された kind: physical - secret: true - id: komori-resold-expired-goods statement: 小森拓は廃棄扱いにした売店商品を帳簿外で知人へ安く売っていた kind: other - secret: true - id: sawada-entered-equipment-room statement: 沢田亮は正式な作業指示を取らずに信号機器室へ入り、個人的に気になっていた旧部品を確認していた kind: other - secret: true - id: body-found-2225 statement: 22時25分、小森拓が駅務室で藤崎正雄の死を発見した kind: observation @@ -89,40 +87,48 @@ timeline: participants: [arima] facts: [arima-skimmed-refunds, fujisaki-found-refund-gap, fujisaki-warned-arima] description: 藤崎が有馬の返金処理の不正を指摘し、終電後の本部報告を告げる。 + location: 駅構内 - id: arima-office-corridor at: "22:05" participants: [arima, sawada] facts: [arima-entered-office-2205, sawada-saw-arima-2205] description: 沢田が駅務室へ向かう有馬を目撃する。 + location: 駅務室前 - id: fujisaki-death at: "22:08" participants: [arima] facts: [arima-killed-fujisaki-2208] description: 有馬が駅務室で藤崎を襲い、藤崎は死亡する。 + location: 駅務室 - id: arima-returns-platform at: "22:12" participants: [arima, komori] facts: [arima-left-office-2212, komori-saw-arima-2212] description: 小森が駅務室側からホームへ戻る有馬を目撃する。 + location: ホーム - id: last-train-departs at: "22:18" participants: [arima, sawada, komori] facts: [last-train-delayed] + record: 運行記録 description: 八分遅れの終電が夕凪駅を出発する。 + location: 駅構内 - id: certificates-print at: "22:20" participants: [arima] facts: [certificates-printed-2220] + record: 印刷履歴 description: 終電出発後、有馬が駅務室の専用プリンターで遅延証明書を一括印刷する。 + location: 駅務室 - id: discovery at: "22:25" participants: [komori, arima, sawada] facts: [body-found-2225] description: 小森が駅務室で藤崎の死を発見する。 + location: 駅務室 characters: - id: arima name: 有馬結衣 - role: suspect publicIntroduction: "接客が丁寧で、遅延時にも落ち着いて対応できる助役。" personality: 接客が丁寧で、遅延時にも落ち着いて対応できる助役。記録と手順を重視する一方、少額の現金差なら見過ごされるという甘さがあった。藤崎には仕事を評価されていたため、不正を見抜かれたことへの動揺が大きい。 goals: @@ -151,7 +157,6 @@ characters: strategy: maintain-until-contradicted memories: - id: refund-warning - about: fujisaki-warned-arima detail: 藤崎に返金処理の一覧を見せられ「終電が出たら本部に報告する」と言われ、時間が残っていないと思った。 relationships: - character: komori @@ -159,7 +164,6 @@ characters: attitude: 売店から駅員の動きをよく見ているので少し警戒している - id: komori name: 小森拓 - role: witness publicIntroduction: "話好きな売店責任者で、ホームや改札の様子をよく見ている。" personality: 話好きな売店責任者で、ホームや改札の様子をよく見ている。廃棄商品の横流しが会社へ知られるのを恐れており、帳簿の話を向けられると急に口数が減る。 goals: @@ -176,7 +180,6 @@ characters: strategy: maintain-until-contradicted memories: - id: arima-returning - about: komori-saw-arima-2212 detail: 終電待ちの客がまだ多い22時12分ごろ、有馬が駅務室側から早足で戻ってきたので、今までどこにいたのかと思った。 relationships: - character: arima @@ -184,7 +187,6 @@ characters: attitude: 普段は几帳面な人だと思っている - id: sawada name: 沢田亮 - role: witness publicIntroduction: "機械好きで、古い鉄道設備を見ると仕事の範囲を越えて確認したくなる点検員。" personality: 機械好きで、古い鉄道設備を見ると仕事の範囲を越えて確認したくなる点検員。無許可で機器室へ入ったことを隠したいが、通路で会った人の時刻は点検時計と結びつけて覚えている。 goals: @@ -201,7 +203,6 @@ characters: strategy: maintain-until-contradicted memories: - id: office-corridor-sighting - about: sawada-saw-arima-2205 detail: 点検時計で22時05分を確認した直後、ホームにいるはずの有馬が駅務室へ向かうのを見た。 relationships: [] revelations: @@ -246,7 +247,6 @@ evidences: label: 遅延証明書プリンターの履歴 description: 遅延証明書は22時20分に一括印刷されており、22時02分から22時18分の間にはまだ紙として存在していない。 reveal: - mode: conversation condition: 有馬に遅延証明書を配った時刻と印刷方法を尋ねるか、小森に終電前の配布状況を確認したら開示する。 sources: - type: character @@ -259,7 +259,6 @@ evidences: label: 二十二時五分の駅務室通路の目撃 description: 沢田は22時05分ごろ、駅務室へ向かう有馬を見ている。 reveal: - mode: conversation condition: 沢田に22時前後の点検中、駅務室側の通路で誰を見たか尋ねたら開示する。 sources: - type: character @@ -270,7 +269,6 @@ evidences: label: 二十二時十二分の売店前の目撃 description: 小森は22時12分ごろ、駅務室側からホームへ戻る有馬を見ている。 reveal: - mode: conversation condition: 小森に終電待ちの間、駅務室側から戻ってきた駅員がいなかったか尋ねたら開示する。 sources: - type: character @@ -281,20 +279,20 @@ evidences: label: 券売機返金処理と現金残高の差 description: 少額返金が繰り返された日時と現金不足が一致し、有馬の担当時間帯へ集中している。 reveal: - mode: conversation - condition: 有馬に藤崎が事件前に確認していた返金処理について尋ねるか、小森に駅長が帳簿を調べていた理由を尋ねたら開示する。 + condition: 有馬に藤崎が事件前に確認していた返金処理について尋ねるか、小森に駅長が帳簿を調べていた理由を尋ねたら開示する。または遺体・現場を調べ、「券売機返金処理と現金残高の差」に関わる資料を確認したら開示する。 sources: - type: character id: arima - type: character id: komori + - type: victim + id: victim supports: [arima-skimmed-refunds, fujisaki-found-refund-gap, fujisaki-warned-arima] contradicts: [] - id: kiosk-disposal-record label: 売店の廃棄記録と帳簿外販売 description: 小森が廃棄扱いの商品を知人へ売っていたことは分かるが、駅務室の事件時刻とは結びつかない。 reveal: - mode: conversation condition: 小森に廃棄商品の扱いを尋ね、帳簿に問題はないと否定したら開示する。 sources: - type: character @@ -305,7 +303,6 @@ evidences: label: 信号機器室の入室記録 description: 沢田が正式な指示なしに機器室へ入っていたことが分かるが、22時05分の有馬の目撃とは矛盾しない。 reveal: - mode: conversation condition: 沢田に指示区域以外へ入っていないか尋ね、否定したため入室記録を確認したら開示する。 sources: - type: character @@ -317,18 +314,9 @@ solution: summary: 犯人は有馬結衣。券売機の返金処理を利用した現金抜き取りを藤崎に見抜かれ、終電後すぐ本部へ報告すると告げられた。有馬は「22時02分から終電が出る22時18分までホームで遅延証明書を配っていた」と話すが、その証明書は22時20分になって初めて印刷されており、主張した時間帯には存在していない。さらに22時05分には沢田が駅務室へ向かう有馬を、22時12分には小森が駅務室側から戻る有馬を見ている。有馬は22時08分ごろ駅務室で藤崎を襲い、終電後に証明書を印刷して配布作業をアリバイへ利用しようとした。小森の帳簿外販売と沢田の無許可入室は独立した隠し事である。 method: ホームで遅延証明書を配っていたという偽の作業アリバイを作り、駅務室で藤崎を襲った後、終電出発後に証明書を印刷した motive: 返金処理を利用した現金抜き取りが本部へ報告され、職と信用を失うことを恐れたため - requiredFacts: [arima-skimmed-refunds, fujisaki-warned-arima, arima-entered-office-2205, sawada-saw-arima-2205, arima-killed-fujisaki-2208, komori-saw-arima-2212, certificates-printed-2220] secretKeywords: - 犯人は有馬 - 有馬が犯人 - 有馬が藤崎を襲 - 私が藤崎を襲 - 遅延証明書をアリバイに殺 -quality: - expectedQuestionCount: - min: 8 - max: 18 - requiredEvidence: - min: 2 - redHerrings: [komori-resold-expired-goods, sawada-entered-equipment-room] - notes: 紙の存在時刻を確認することで「配布していた」という作業アリバイを崩す。沢田と小森の二方向の目撃があるため、どちらか一人への質問を逃しても有馬が駅務室へ出入りしたことへ到達できる。 diff --git a/db/scenarios/mars-outpost-delayed-reply.yaml b/db/scenarios/mars-outpost-delayed-reply.yaml index 12ccd9e..dc80e38 100644 --- a/db/scenarios/mars-outpost-delayed-reply.yaml +++ b/db/scenarios/mars-outpost-delayed-reply.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: mars-outpost-delayed-reply meta: - title: 火星基地、砂嵐の夜 + title: "火星基地に雨は降らない" synopsis: "2147年、火星エリュシオン観測基地。大規模な砂嵐で地表車両も通信中継ドローンも停止するなか、主任研究員エレナ・ヴァルガが解析室で死亡しました。" category: SFクローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [2147年, 火星, 通信遅延, 基地] victim: name: エレナ・ヴァルガ introduction: 火星エリュシオン観測基地主任研究員 + foundAt: 21:50 + foundIn: 解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: エレナ・ヴァルガは解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「調達予算の監査ファイル」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -47,34 +54,27 @@ facts: - id: noah-budget-fraud statement: ノア・チェンは観測機器の調達予算を別用途へ流用していた kind: motive - secret: true - id: elena-found-fraud statement: エレナ・ヴァルガは事件当日、ノアによる予算流用を発見した kind: motive - secret: true - id: elena-would-report statement: 21時25分、エレナはノアに翌朝の定時通信で地球本部へ不正を報告すると告げた kind: motive - secret: true - id: luis-saw-noah-corridor statement: 21時33分ごろ、ルイス・オルテガは解析室へ続く連絡廊下でノア・チェンを見た kind: observation - id: noah-killed-elena statement: 21時36分ごろ、ノア・チェンは解析室でエレナを襲い死亡させた kind: truth - secret: true - id: noah-played-arrival-tone statement: 21時38分、ノア・チェンは地球から届いた返信を自分がその場で会話していた証拠として利用した kind: truth - secret: true - id: amira-private-channel statement: アミラ・サイードは規則に反して家族との私的通信へ基地帯域を使っていた kind: other - secret: true - id: luis-unlogged-part statement: ルイス・オルテガは予備部品を正式な在庫登録なしで交換していた kind: other - secret: true - id: body-found-2150 statement: 21時50分、アミラ・サイードが解析室でエレナの死を発見した kind: observation @@ -84,35 +84,41 @@ timeline: participants: [noah, amira] facts: [noah-sent-question-2118] description: ノアが地球管制へ観測予算について質問を送る。 + location: 基地内 - id: fraud-confrontation at: "21:25" participants: [noah] facts: [noah-budget-fraud, elena-found-fraud, elena-would-report] description: エレナが予算流用をノアへ突きつけ、翌朝に地球本部へ報告すると告げる。 + location: 基地内 - id: corridor-sighting at: "21:33" participants: [noah, luis] facts: [luis-saw-noah-corridor] description: ルイスが解析室へ続く連絡廊下でノアを見かける。 + location: 連絡廊下 - id: elena-death at: "21:36" participants: [noah] facts: [noah-killed-elena] description: ノアが解析室でエレナを襲う。 + location: 解析室 - id: delayed-reply at: "21:38" participants: [noah, amira] facts: [earth-reply-arrived-2138, noah-played-arrival-tone, earth-mars-delay] + record: 通信記録 description: 21時18分の質問への返答が地球から到着し、ノアはそれを事件時刻の会話記録として扱わせる。 + location: 基地内 - id: discovery at: "21:50" participants: [noah, amira, luis] facts: [body-found-2150] description: アミラが解析室でエレナの死を発見する。 + location: 解析室 characters: - id: noah name: ノア・チェン - role: suspect publicIntroduction: "火星基地の副主任。" personality: 冷静で数字に強い副主任。地球からの返信が21時38分に届いたことを強調し、それを自分が通信室にいた証明として扱わせようとする。予算の細部には神経質。 goals: @@ -141,7 +147,6 @@ characters: strategy: maintain-until-contradicted memories: - id: noah-report-threat - about: elena-would-report detail: エレナに「朝の窓で本部へ全部送る」と言われた瞬間、地球へ届けば取り返せないと感じた。 relationships: - character: amira @@ -152,7 +157,6 @@ characters: attitude: 廊下で自分を見た可能性があり避けたい - id: amira name: アミラ・サイード - role: witness publicIntroduction: "火星基地の通信士。" personality: 理屈を重んじる通信士。送受信時刻と会話の因果関係を厳密に区別する。私的通信の帯域利用は隠したい。 goals: @@ -169,7 +173,6 @@ characters: strategy: maintain-until-contradicted memories: - id: amira-packet-chain - about: earth-reply-arrived-2138 detail: 21時38分のパケットには、21時18分に送った質問の識別番号がそのまま返っていた。 relationships: - character: noah @@ -180,7 +183,6 @@ characters: attitude: 機械は雑に扱うが人の動きはよく見ていると思っている - id: luis name: ルイス・オルテガ - role: suspect publicIntroduction: "現場主義の整備主任。" personality: 現場主義の整備主任。規定より稼働を優先して無登録部品を使うことがある。通信には疎いが、廊下で見たノアについては自信がある。 goals: @@ -197,7 +199,6 @@ characters: strategy: maintain-until-contradicted memories: - id: luis-noah-corridor - about: luis-saw-noah-corridor detail: 解析室へ工具を取りに向かったとき、逆方向から来るノアとすれ違った。 relationships: - character: noah @@ -238,7 +239,6 @@ evidences: label: 惑星間通信のスレッド記録 description: 21時38分の返信パケットには21時18分送信の質問IDが紐づき、約20分前の問いへの返答だと分かる。 reveal: - mode: conversation condition: アミラかノアに21時38分の返信がどの質問への返答だったか具体的に尋ねたら開示する。 sources: - { type: character, id: amira } @@ -249,7 +249,6 @@ evidences: label: 21時33分の整備メモ description: ルイスの整備端末に21時33分の位置メモがあり、その場所で解析室側から来たノアとすれ違ったと記録されている。 reveal: - mode: conversation condition: ルイスに21時30分台の作業場所と誰を見たか尋ねたら開示する。 sources: - { type: character, id: luis } @@ -259,18 +258,17 @@ evidences: label: 調達予算の監査ファイル description: ノア管理の予算だけ用途が合わず、エレナが翌朝の地球送信用フォルダへ証拠をまとめている。 reveal: - mode: conversation - condition: ノアかアミラにエレナが翌朝送ろうとしていた監査資料について尋ねたら開示する。 + condition: ノアかアミラにエレナが翌朝送ろうとしていた監査資料について尋ねたら開示する。または遺体・現場を調べ、「調達予算の監査ファイル」に関わる資料を確認したら開示する。 sources: - { type: character, id: noah } - { type: character, id: amira } + - { type: victim, id: victim } supports: [noah-budget-fraud, elena-found-fraud, elena-would-report] contradicts: ["lie:noah-no-fraud"] - id: private-bandwidth-log label: 私用通信の帯域ログ description: アミラの私用通信が見つかるが、解析室の事件とは独立している。 reveal: - mode: conversation condition: アミラに業務外の通信帯域利用を追及したら開示する。 sources: - { type: character, id: amira } @@ -281,14 +279,8 @@ solution: summary: 犯人はノア・チェン。予算流用をエレナに見抜かれ、翌朝地球本部へ報告されるのを恐れた。ノアは21時18分に地球へ質問を送った後、返信を待つ約20分の間に通信室を離れ、21時36分ごろ解析室でエレナを襲った。21時38分に地球から返答が届くと、それを「その時刻に地球と会話していた」証拠として利用した。しかし返信は21時18分の質問へのもので、受信時にノアが端末前にいる必要はない。さらに21時33分にはルイスが解析室側でノアを目撃している。 method: 惑星間通信の受信時刻を現在進行の会話時刻に見せかけ、返信待ちの時間帯の行動を隠した motive: 予算流用が地球本部へ報告されるのを防ぐため - requiredFacts: [earth-mars-delay, noah-sent-question-2118, earth-reply-arrived-2138, noah-budget-fraud, elena-would-report, luis-saw-noah-corridor, noah-killed-elena, noah-played-arrival-tone] secretKeywords: - 犯人はノア - ノアがエレナを襲 - 返信をアリバイに - 私がエレナを襲 -quality: - expectedQuestionCount: { min: 10, max: 22 } - requiredEvidence: { min: 3 } - redHerrings: [amira-private-channel, luis-unlogged-part] - notes: 受信パケットが存在することと、その瞬間に通信相手が端末前にいたことを分離する。通信遅延と廊下目撃の二段でアリバイを崩す。 diff --git a/db/scenarios/midnight-radio-rerun.yaml b/db/scenarios/midnight-radio-rerun.yaml index 0239676..c9224e6 100644 --- a/db/scenarios/midnight-radio-rerun.yaml +++ b/db/scenarios/midnight-radio-rerun.yaml @@ -1,15 +1,35 @@ schemaVersion: 1 id: midnight-radio-rerun meta: - title: ラジオ局レイライン、深夜の事件 - synopsis: "午前零時十二分、FMラジオ局「レイライン」の第二収録ブースで、人気パーソナリティの大門修一が死亡しているのが見つかりました。局内に残っていたのは、番組プロデューサーの美濃部沙耶、音響技師の久世直人、スポンサー会社の担当者・夏目亮介の三人です。" + title: "零時放送レイライン" + synopsis: "午前零時十二分、FMラジオ局「レイライン」の第二収録ブースで、人気パーソナリティの大門修一が死亡しているのが見つかりました。局内に残って\ + いたのは、番組プロデューサーの美濃部沙耶、音響技師の久世直人、スポンサー会社の担当者・夏目亮介の三人です。" category: 放送局ミステリ difficulty: 3 estimatedMinutes: 10 - tags: [ラジオ, 時刻トリック, 偽装アリバイ] victim: name: 大門修一 introduction: FMラジオ局「レイライン」パーソナリティ + foundAt: 00:12 + foundIn: 第2ブース + estimatedDeathAt: "23:48" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 大門修一は第2ブースで倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「大門が保存したスポンサー報告の比較メモ」に関わる資料が残されている。 +places: + - id: playout-room + name: 自動送出室 + shortName: 送出室 + introduction: 収録音源と深夜番組の放送順を管理する送出卓 + situation: 送出卓のモニターと操作盤が待機状態になっている + findings: + - id: midnight-queued-audio + statement: 午前零時の番組冒頭には、事前収録された音声ファイルが自動送出対象として登録されている。 + - id: playout-execution-log + statement: 実行ログでは、午前零時の音声は人のマイク操作ではなく送出卓から自動再生されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,37 +58,30 @@ facts: - id: opening-recorded-2320 statement: 23時20分、大門修一は午前零時に流す番組冒頭約五分間を事前収録した kind: truth - secret: true - id: minobe-falsified-reports statement: 美濃部沙耶はスポンサー向けの放送実績報告を水増しし、制作費の一部を別用途へ流用していた kind: motive - secret: true - id: daimon-confronted-minobe statement: 大門修一はその夜、美濃部沙耶の不正を知り、翌朝局長へ報告すると告げていた kind: motive - secret: true - id: natsume-secret-meeting statement: 23時38分から23時43分まで、夏目亮介は大門修一と第二収録ブースでスポンサー契約を巡って口論した kind: observation - secret: true - id: natsume-left-2343 statement: 23時43分、夏目亮介は第二収録ブースを出て一階ロビーへ向かった kind: observation - id: minobe-entered-2346 statement: 23時46分、美濃部沙耶は第二収録ブースへ入った kind: truth - secret: true - id: daimon-died-2348 statement: 23時48分ごろ、大門修一は第二収録ブース内で美濃部沙耶に襲われ死亡した kind: truth - secret: true - id: kuze-saw-minobe-2350 statement: 23時50分ごろ、久世直人は第二収録ブース側の廊下から戻ってくる美濃部沙耶を見た kind: observation - id: minobe-queued-recording statement: 23時53分、美濃部沙耶は23時20分に録音された大門修一の音声を午前零時の自動送出枠へ登録した kind: truth - secret: true - id: recorded-opening-aired statement: 午前零時、大門修一の事前収録された声が自動送出され、番組冒頭として放送された kind: physical @@ -78,67 +91,80 @@ facts: - id: kuze-deleted-demo statement: 久世直人は勤務中に私用で録音した音源を隠すため、事件直前に一件のテストデータを削除していた kind: other - secret: true - id: natsume-offered-side-payment statement: 夏目亮介は番組継続を有利にするため、大門修一へ個人的な謝礼を持ちかけて拒絶されていた kind: motive - secret: true timeline: - id: opening-recording at: "23:20" - participants: [minobe, kuze] - facts: [opening-recorded-2320] + participants: [ minobe, kuze ] + facts: [ opening-recorded-2320 ] description: 大門が午前零時に流す番組冒頭を事前収録する。 + location: 局内 - id: natsume-meets-daimon at: "23:38" - participants: [natsume] - facts: [natsume-secret-meeting] + participants: [ natsume ] + facts: [ natsume-secret-meeting ] description: 夏目が第二収録ブースで大門とスポンサー契約を巡って口論する。 + location: 第2ブース - id: natsume-leaves at: "23:43" - participants: [natsume] - facts: [natsume-left-2343] + participants: [ natsume ] + facts: [ natsume-left-2343 ] description: 夏目が大門のもとを離れ、一階ロビーへ向かう。 + location: ロビー - id: minobe-enters at: "23:46" - participants: [minobe] - facts: [minobe-entered-2346] + participants: [ minobe ] + facts: [ minobe-entered-2346 ] description: 美濃部が第二収録ブースへ入る。 + location: 第2ブース - id: daimon-death at: "23:48" - participants: [minobe] - facts: [daimon-died-2348] + participants: [ minobe ] + facts: [ daimon-died-2348 ] description: 美濃部が大門を襲い、大門はブース内で死亡する。 + location: ブース - id: minobe-returns at: "23:50" - participants: [minobe, kuze] - facts: [kuze-saw-minobe-2350] + participants: [ minobe, kuze ] + facts: [ kuze-saw-minobe-2350 ] description: 久世が第二収録ブース側の廊下から戻る美濃部を目撃する。 + location: 廊下 - id: recording-queued at: "23:53" - participants: [minobe] - facts: [minobe-queued-recording] + participants: [ minobe ] + facts: [ minobe-queued-recording ] description: 美濃部が事前収録音声を午前零時の自動送出枠へ登録する。 + location: 送出室 - id: opening-airs at: "00:00" - participants: [kuze, minobe, natsume] - facts: [recorded-opening-aired] + participants: [ kuze, minobe, natsume ] + facts: [ recorded-opening-aired ] + record: 自動送出ログ description: 事前収録された大門の声が番組冒頭として放送される。 + location: 局内 - id: discovery at: "00:12" - participants: [kuze, minobe, natsume] - facts: [body-found-0012] + participants: [ kuze, minobe, natsume ] + facts: [ body-found-0012 ] description: 久世が第二収録ブースで大門の死を発見する。 + location: 第2ブース characters: - id: minobe name: 美濃部沙耶 - role: suspect publicIntroduction: "判断が速く、番組進行の乱れを何より嫌うプロデューサー。" personality: 判断が速く、番組進行の乱れを何より嫌うプロデューサー。普段は冷静だが、制作費や報告書の話になると妙に説明が細かくなる。大門とは長年組んできた。 goals: - 放送実績報告の不正を隠し通したい - 午前零時まで大門が生きていたと思わせたい - knowledge: [minobe-is-producer, opening-recorded-2320, recorded-opening-aired, body-found-0012] + knowledge: + [ + minobe-is-producer, + opening-recorded-2320, + recorded-opening-aired, + body-found-0012 + ] secrets: - fact: minobe-falsified-reports disclosure: pressured @@ -157,7 +183,6 @@ characters: strategy: maintain-until-contradicted memories: - id: daimon-warning - about: daimon-confronted-minobe detail: 大門に「明日の朝、全部局長に話す」と低い声で告げられた瞬間を鮮明に覚えている。 relationships: - character: kuze @@ -165,13 +190,19 @@ characters: attitude: 技術は信用しているが、私用録音の癖には苛立っている - id: kuze name: 久世直人 - role: witness publicIntroduction: "ラジオ局の音響技師。" personality: 機材の話になると饒舌だが、人間関係には深入りしたがらない音響技師。自分の小さな規則違反を大事にされるのを恐れている。 goals: - 私用録音をしていたことを隠したい - 見たことだけは正確に伝えたい - knowledge: [kuze-is-engineer, opening-recorded-2320, kuze-saw-minobe-2350, recorded-opening-aired, body-found-0012] + knowledge: + [ + kuze-is-engineer, + opening-recorded-2320, + kuze-saw-minobe-2350, + recorded-opening-aired, + body-found-0012 + ] secrets: - fact: kuze-deleted-demo disclosure: pressured @@ -182,21 +213,27 @@ characters: strategy: maintain-until-contradicted memories: - id: hallway-glance - about: kuze-saw-minobe-2350 detail: 23時50分ごろ、廊下の角から戻ってきた美濃部と目が合い、珍しく彼女が驚いた顔をしたのを覚えている。 + - id: death-estimate-memory + detail: 第2ブースの空調温度と発見時の状態を確認しており、死亡は23時48分ごろと見積もられるという確認内容を覚えている。 relationships: - character: minobe relation: 同僚 attitude: 仕事はできる人だと思っている - id: natsume name: 夏目亮介 - role: suspect publicIntroduction: "愛想のよい営業担当だが、契約の話になると押しが強い。" personality: 愛想のよい営業担当だが、契約の話になると押しが強い。会社に知られたくない交渉を抱えており、自分が大門と揉めていた事実を必死に軽く見せようとする。 goals: - 大門へ個人的な謝礼を持ちかけたことを隠したい - 23時台に大門と口論した事実を伏せたい - knowledge: [natsume-is-sponsor, natsume-left-2343, recorded-opening-aired, body-found-0012] + knowledge: + [ + natsume-is-sponsor, + natsume-left-2343, + recorded-opening-aired, + body-found-0012 + ] secrets: - fact: natsume-secret-meeting disclosure: pressured @@ -209,7 +246,6 @@ characters: strategy: maintain-until-contradicted memories: - id: rejected-offer - about: natsume-offered-side-payment detail: 謝礼の話を出した途端、大門に「そういう仕事はしない」と切り捨てられた悔しさが残っている。 relationships: [] revelations: @@ -226,8 +262,8 @@ revelations: revealCondition: 久世に午前零時の放送が本当に生だったのか、送出設備の記録を含めて確認した。 requires: revelations: [] - evidences: [automation-log] - relatedFacts: [opening-recorded-2320, recorded-opening-aired] + evidences: [ automation-log ] + relatedFacts: [ opening-recorded-2320, recorded-opening-aired ] - id: minobe-under-pressure title: 翌朝に露見するはずだった不正 text: 大門は美濃部の放送実績報告の水増しを知り、翌朝局長へ報告すると告げていた。 @@ -240,86 +276,86 @@ revelations: id: minobe revealCondition: 美濃部にスポンサー報告と大門との直前の会話を追及し、翌朝の報告を恐れていたことが明確になった。 requires: - revelations: [recorded-not-live] - evidences: [report-draft] - relatedFacts: [minobe-falsified-reports, daimon-confronted-minobe] + revelations: [ recorded-not-live ] + evidences: [ report-draft ] + relatedFacts: [ minobe-falsified-reports, daimon-confronted-minobe ] evidences: - id: automation-log label: 自動送出システムの実行ログ description: 午前零時の冒頭素材は23時53分に登録され、時刻指定で自動再生された記録が残る。 reveal: - mode: conversation - condition: 久世に午前零時の音声が生放送か録音かを尋ねるか、美濃部の「生だった」という説明の技術的根拠を確認したら開示する。 + condition: 久世に午前零時の音声が生放送か録音かを尋ねるか、美濃部の「生だった」という説明の技術的根拠を確認したら開示する。または自動送出室を調べ、午前零時の送出キューと実行ログを確認したら開示する。 sources: - type: character id: kuze - type: character id: minobe - supports: [minobe-queued-recording, recorded-opening-aired] - contradicts: ["lie:minobe-live-alibi"] + - { type: location, id: playout-room } + supports: [ minobe-queued-recording, recorded-opening-aired ] + contradicts: [ "lie:minobe-live-alibi" ] - id: corridor-sighting label: 23時50分の廊下の目撃 description: 久世は第二収録ブース側から戻ってくる美濃部を23時50分ごろ目撃している。 reveal: - mode: conversation condition: 久世に23時45分から23時55分ごろ廊下で誰を見たか尋ね、美濃部を見たという証言が出たら開示する。 sources: - type: character id: kuze - supports: [kuze-saw-minobe-2350] - contradicts: ["lie:minobe-live-alibi"] + supports: [ kuze-saw-minobe-2350 ] + contradicts: [ "lie:minobe-live-alibi" ] - id: lobby-camera label: 一階ロビーの入退室映像 description: 夏目が23時43分にロビーへ戻ったことが確認できるが、それ以前の数分間はロビーにいない。 reveal: - mode: conversation condition: 夏目の23時台の行動を具体的に確認し、ずっとロビーにいたという説明を検証しようとしたら開示する。 sources: - type: character id: natsume - supports: [natsume-secret-meeting, natsume-left-2343] - contradicts: ["lie:natsume-left-early"] + supports: [ natsume-secret-meeting, natsume-left-2343 ] + contradicts: [ "lie:natsume-left-early" ] - id: report-draft label: 大門が保存したスポンサー報告の比較メモ description: 公式報告と実際の放送枠に差があり、美濃部の報告水増しを大門が確認していたことが分かる。 reveal: - mode: conversation - condition: 美濃部に制作費やスポンサー報告の不一致について尋ねるか、夏目に大門が最近スポンサー実績を調べていなかったか尋ねたら開示する。 + condition: 美濃部に制作費やスポンサー報告の不一致について尋ねるか、夏目に大門が最近スポンサー実績を調べていなかったか尋ねたら開示する。または遺体・現場を調べ、「大門が保存したスポンサー報告の比較メモ」に関わる資料を確認したら開示する。 sources: - type: character id: minobe - type: character id: natsume - supports: [minobe-falsified-reports, daimon-confronted-minobe] + - type: victim + id: victim + supports: [ minobe-falsified-reports, daimon-confronted-minobe ] contradicts: [] - id: deleted-demo-log label: 削除された私用テスト音源の履歴 description: 久世が事件前に私用録音を削除していたことだけが分かり、殺害時刻とは結びつかない。 reveal: - mode: conversation condition: 久世に送出ログや削除履歴を細かく確認し、事件前に消したデータがないか追及したら開示する。 sources: - type: character id: kuze - supports: [kuze-deleted-demo] - contradicts: ["lie:kuze-no-deletion"] + supports: [ kuze-deleted-demo ] + contradicts: [ "lie:kuze-no-deletion" ] + - id: death-estimate + label: 第2ブースの死亡推定 + description: 第2ブースの一定した室温と発見時の状態を合わせると、大門の死亡は23時48分ごろと見積もられる。午前零時の放送より前である。 + reveal: + condition: 遺体を調べて発見時の状態を確認するか、久世にブースの室温記録と発見時の確認内容を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: kuze } + supports: [daimon-died-2348] + contradicts: [] + revealsDeathTime: true solution: culprit: minobe summary: 犯人は美濃部沙耶。スポンサー向け報告の水増しを大門に知られ、翌朝局長へ報告されることを恐れた美濃部は、夏目が去った後の23時46分に第二収録ブースへ入り、23時48分ごろ大門を襲った。その後23時20分に録音済みだった大門の冒頭音声を23時53分に自動送出へ登録し、午前零時まで大門が生きていたように見せかけた。自動送出ログは「午前零時は生放送だった」という美濃部の説明を崩し、久世の23時50分の目撃は「23時40分以降ブースへ行っていない」という主張と矛盾する。夏目の秘密の面会と久世の私用録音は、それぞれ独立した隠し事にすぎない。 method: 第二収録ブースで大門を襲い、死亡後に事前収録音声を自動送出して生存時刻を偽装した motive: 放送実績報告の不正が翌朝露見することへの恐れ - requiredFacts: [opening-recorded-2320, minobe-falsified-reports, daimon-confronted-minobe, minobe-entered-2346, daimon-died-2348, minobe-queued-recording, kuze-saw-minobe-2350] secretKeywords: - 犯人は美濃部 - 美濃部が犯人 - 美濃部が大門を襲 - 私が大門を襲 - 録音で死亡時刻を偽装 -quality: - expectedQuestionCount: - min: 8 - max: 18 - requiredEvidence: - min: 2 - redHerrings: [natsume-secret-meeting, natsume-offered-side-payment, kuze-deleted-demo] - notes: 主経路は自動送出ログで午前零時の声をアリバイから外し、久世の23時50分の目撃と美濃部の不在主張を衝突させること。夏目と久世にも明確な嘘を持たせ、秘密の有無だけで犯人を判別できないようにする。 diff --git a/db/scenarios/polar-station-shared-clock.yaml b/db/scenarios/polar-station-shared-clock.yaml index 61177a6..4a7e14e 100644 --- a/db/scenarios/polar-station-shared-clock.yaml +++ b/db/scenarios/polar-station-shared-clock.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: polar-station-shared-clock meta: - title: 白夜第六観測基地、吹雪の夜 - synopsis: "午後十一時十分、吹雪で完全に孤立した白夜第六観測基地の解析室で、主任研究員の牧瀬航が死亡しているのが見つかりました。基地内にいたのは観測員の篠宮怜、通信担当の樋口海、整備担当の沢渡直人の三人だけです。" + title: "白夜第六基地の三人" + synopsis: "午後十一時十分、吹雪で完全に孤立した白夜第六観測基地の解析室で、主任研究員の牧瀬航が死亡しているのが見つかりました。基地内にいたのは観測\ + 員の篠宮怜、通信担当の樋口海、整備担当の沢渡直人の三人だけです。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [極地基地, 時計, 時刻, 相関した証拠] victim: name: 牧瀬航 introduction: 白夜第六観測基地主任研究員 + foundAt: 23:10 + foundIn: 解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 牧瀬航は解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「翌朝の研究会議議題」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,7 +46,6 @@ facts: - id: central-clock-offset statement: 21時50分の同期障害以降、基地の中央時刻系は実際の時刻より七分進んだ状態で固定されていた kind: physical - secret: true - id: terminal-uses-central-clock statement: 通信端末の操作履歴は基地の中央時刻系を参照している kind: physical @@ -66,66 +73,87 @@ facts: - id: makise-death-real-2223 statement: 実際の22時23分ごろ、牧瀬航は解析室で襲われ死亡した kind: truth - secret: true - id: shinomiya-killed-makise statement: 篠宮怜は実際の22時23分ごろ解析室で牧瀬航を襲い死亡させた kind: truth - secret: true - id: data-manipulation-found statement: 牧瀬航は篠宮怜が観測結果の一部を恣意的に除外していたことに気づいた kind: motive - secret: true - id: disclosure-next-morning statement: 牧瀬航は翌朝の研究会議で篠宮怜の不適切なデータ除外を報告する予定だった kind: motive - secret: true - id: higuchi-hid-sync-failure statement: 樋口海は同期障害の初動対応が遅れたことを責められるのを恐れ、障害発生時刻をすぐ共有しなかった kind: other - secret: true - id: sawatari-used-extra-power statement: 沢渡直人は許可なく私物機器を基地電源へ接続していた kind: other - secret: true - id: body-found-2310 statement: 23時10分、樋口海が解析室で牧瀬航の死を発見した kind: observation timeline: - id: sync-failure at: "21:50" - participants: [higuchi] - facts: [central-clock-offset, higuchi-hid-sync-failure] + participants: [ higuchi ] + facts: [ central-clock-offset, higuchi-hid-sync-failure ] + record: 同期ずれ記録 description: 時刻同期系に障害が起き、基地の中央時刻系が実際より七分進んだ状態になる。 + location: 基地内 - id: false-alibi-window at: "22:13" - participants: [shinomiya] - facts: [terminal-uses-central-clock, access-uses-central-clock, observation-log-central-clock, wall-clock-synced, four-times-not-independent] + participants: [ shinomiya ] + facts: + [ + terminal-uses-central-clock, + access-uses-central-clock, + observation-log-central-clock, + wall-clock-synced, + four-times-not-independent + ] + record: 入室履歴 description: 中央時刻系のずれを共有する複数の記録が、篠宮の通信区画滞在を実際より七分遅い時刻として残す。 + location: 基地内 - id: real-corridor-sighting at: "22:20" - participants: [shinomiya, sawatari] - facts: [handheld-clock-correct, sawatari-saw-shinomiya-real-2220, displayed-time-was-2227] + participants: [ shinomiya, sawatari ] + facts: + [ + handheld-clock-correct, + sawatari-saw-shinomiya-real-2220, + displayed-time-was-2227 + ] + record: 整備時刻メモ description: 沢渡が独立した携帯時計で22時20分を確認し、解析室側の通路にいる篠宮を目撃する。 + location: 通路 - id: makise-death at: "22:23" - participants: [shinomiya] - facts: [makise-death-real-2223, shinomiya-killed-makise] + participants: [ shinomiya ] + facts: [ makise-death-real-2223, shinomiya-killed-makise ] description: 篠宮が解析室で牧瀬を襲い、牧瀬は死亡する。 + location: 解析室 - id: discovery at: "23:10" - participants: [higuchi, shinomiya, sawatari] - facts: [body-found-2310] + participants: [ higuchi, shinomiya, sawatari ] + facts: [ body-found-2310 ] description: 樋口が解析室で牧瀬の死を発見する。 + location: 解析室 characters: - id: shinomiya name: 篠宮怜 - role: suspect publicIntroduction: "理詰めで話す観測員。" personality: 理詰めで話す観測員。記録が複数一致していることを強く主張し、個人の目撃よりシステムログを信用すべきだと言う。研究成果を失うことへの焦りが強い。 goals: - 四種類の時刻記録を独立したアリバイ証拠に見せたい - 観測データを恣意的に除外していたことを隠したい - knowledge: [shinomiya-observer, terminal-uses-central-clock, access-uses-central-clock, observation-log-central-clock, wall-clock-synced, body-found-2310] + knowledge: + [ + shinomiya-observer, + terminal-uses-central-clock, + access-uses-central-clock, + observation-log-central-clock, + wall-clock-synced, + body-found-2310 + ] secrets: - fact: data-manipulation-found disclosure: pressured @@ -140,10 +168,8 @@ characters: strategy: maintain-until-contradicted memories: - id: knew-clock-dependence - about: four-times-not-independent detail: 通信区画の端末も入室履歴も観測表示も壁時計も、同じ中央時刻系を参照していることは日常業務で知っている。 - id: research-meeting-fear - about: disclosure-next-morning detail: 牧瀬から「明日の会議で除外処理の理由を全員に説明してもらう」と告げられ、これまでの研究が崩れると感じた。 relationships: - character: higuchi @@ -154,13 +180,21 @@ characters: attitude: 携帯時計で時刻を確認する癖を軽視していた - id: higuchi name: 樋口海 - role: witness publicIntroduction: "極地観測基地の通信担当。" personality: 神経質な通信担当。システム障害を自分の失態と考え、最初は同期ずれを小さく見せようとする。技術仕様については正確に説明できる。 goals: - 同期障害の共有が遅れたことを隠したい - 四種類の記録が同じ時刻源を使っていたことは正確に伝えたい - knowledge: [higuchi-comms, terminal-uses-central-clock, access-uses-central-clock, observation-log-central-clock, wall-clock-synced, four-times-not-independent, body-found-2310] + knowledge: + [ + higuchi-comms, + terminal-uses-central-clock, + access-uses-central-clock, + observation-log-central-clock, + wall-clock-synced, + four-times-not-independent, + body-found-2310 + ] secrets: - fact: central-clock-offset disclosure: pressured @@ -173,7 +207,6 @@ characters: strategy: maintain-until-contradicted memories: - id: seven-minute-offset - about: central-clock-offset detail: 障害確認時に中央時計が七分進んでいるのを見つけたが、対応の遅れを知られたくなくて全員への連絡を後回しにした。 relationships: - character: shinomiya @@ -181,13 +214,19 @@ characters: attitude: システムに詳しい篠宮なら四記録が独立していないことを知っているはずだと思っている - id: sawatari name: 沢渡直人 - role: witness publicIntroduction: "現場感覚を重視する整備員。" personality: 現場感覚を重視する整備員。基地システムとは別の携帯時計を使う習慣があり、自分の見た時刻に自信がある。私物機器の無断使用だけは隠したい。 goals: - 私物機器の無断使用を隠したい - 22時20分の篠宮の目撃時刻が独立した時計によるものだと説明したい - knowledge: [sawatari-maintenance, handheld-clock-correct, sawatari-saw-shinomiya-real-2220, displayed-time-was-2227, body-found-2310] + knowledge: + [ + sawatari-maintenance, + handheld-clock-correct, + sawatari-saw-shinomiya-real-2220, + displayed-time-was-2227, + body-found-2310 + ] secrets: - fact: sawatari-used-extra-power disclosure: pressured @@ -198,7 +237,6 @@ characters: strategy: maintain-until-contradicted memories: - id: independent-watch - about: sawatari-saw-shinomiya-real-2220 detail: 発電設備の点検時刻を記録するため携帯時計を見た直後、解析室側から来る篠宮とすれ違ったので22時20分だったと断言できる。 relationships: - character: shinomiya @@ -218,14 +256,20 @@ revelations: revealCondition: 樋口に四種類の時刻記録がそれぞれ何を時刻源にしているかと、同期障害のずれ幅を追及した。 requires: revelations: [] - evidences: [clock-source-diagram] + evidences: [ clock-source-diagram ] - type: character id: sawatari revealCondition: 沢渡に携帯時計が基地時刻と同期しているか尋ね、22時20分の目撃が独立した時計に基づくと確認した。 requires: revelations: [] - evidences: [handheld-watch-note] - relatedFacts: [central-clock-offset, four-times-not-independent, handheld-clock-correct, sawatari-saw-shinomiya-real-2220] + evidences: [ handheld-watch-note ] + relatedFacts: + [ + central-clock-offset, + four-times-not-independent, + handheld-clock-correct, + sawatari-saw-shinomiya-real-2220 + ] - id: research-disclosure-motive title: 翌朝の研究会議 text: 牧瀬は篠宮による恣意的なデータ除外を見つけ、翌朝の研究会議で説明させる予定だった。篠宮には研究成果と立場を失う恐れがあった。 @@ -238,97 +282,96 @@ revelations: id: shinomiya revealCondition: 篠宮に除外した観測データと翌朝の研究会議について追及し、牧瀬から説明を求められていたことを認めさせた。 requires: - revelations: [four-clocks-one-source] - evidences: [meeting-agenda] - relatedFacts: [data-manipulation-found, disclosure-next-morning] + revelations: [ four-clocks-one-source ] + evidences: [ meeting-agenda ] + relatedFacts: [ data-manipulation-found, disclosure-next-morning ] evidences: - id: clock-source-diagram label: 基地時刻系の接続図 description: 通信端末、入室履歴、観測ログ、通信区画前の壁時計がすべて同じ中央時刻系を参照していることが分かる。 reveal: - mode: conversation condition: 樋口か篠宮に四種類の時刻記録がどの時計を参照しているか具体的に尋ねたら開示する。 sources: - type: character id: higuchi - type: character id: shinomiya - supports: [terminal-uses-central-clock, access-uses-central-clock, observation-log-central-clock, wall-clock-synced, four-times-not-independent] - contradicts: ["lie:shinomiya-four-clock-alibi"] + supports: + [ + terminal-uses-central-clock, + access-uses-central-clock, + observation-log-central-clock, + wall-clock-synced, + four-times-not-independent + ] + contradicts: [ "lie:shinomiya-four-clock-alibi" ] - id: sync-error-record label: 七分の同期ずれ記録 description: 21時50分以降、中央時刻系が実際より七分進んだ状態だったことが保守記録から確認できる。 reveal: - mode: conversation condition: 樋口に同期障害の実際のずれ幅と発生時刻を追及したら開示する。 sources: - type: character id: higuchi - supports: [central-clock-offset] - contradicts: ["lie:higuchi-no-serious-offset", "lie:shinomiya-four-clock-alibi"] + supports: [ central-clock-offset ] + contradicts: [ "lie:higuchi-no-serious-offset", "lie:shinomiya-four-clock-alibi" ] - id: handheld-watch-note label: 沢渡の整備時刻メモ description: 基地時刻系と同期しない携帯時計で、沢渡は22時20分に解析室側の通路で篠宮を見たと記録している。 reveal: - mode: conversation condition: 沢渡に22時20分の目撃時刻を何で確認したか尋ねたら開示する。 sources: - type: character id: sawatari - supports: [handheld-clock-correct, sawatari-saw-shinomiya-real-2220, displayed-time-was-2227] - contradicts: ["lie:shinomiya-four-clock-alibi"] + supports: + [ + handheld-clock-correct, + sawatari-saw-shinomiya-real-2220, + displayed-time-was-2227 + ] + contradicts: [ "lie:shinomiya-four-clock-alibi" ] - id: meeting-agenda label: 翌朝の研究会議議題 description: 牧瀬の会議メモに、篠宮によるデータ除外の妥当性を全員の前で確認する予定が記されている。 reveal: - mode: conversation - condition: 篠宮か樋口に翌朝の研究会議で牧瀬が扱う予定だった議題を尋ねたら開示する。 + condition: 篠宮か樋口に翌朝の研究会議で牧瀬が扱う予定だった議題を尋ねたら開示する。または遺体・現場を調べ、「翌朝の研究会議議題」に関わる資料を確認したら開示する。 sources: - type: character id: shinomiya - type: character id: higuchi - supports: [data-manipulation-found, disclosure-next-morning] + - type: victim + id: victim + supports: [ data-manipulation-found, disclosure-next-morning ] contradicts: [] - id: hidden-sync-delay label: 障害共有の遅延 description: 樋口が同期障害の共有を遅らせていたことが分かるが、牧瀬の死とは独立した隠し事である。 reveal: - mode: conversation condition: 樋口に同期障害をいつ全員へ共有したか追及したら開示する。 sources: - type: character id: higuchi - supports: [higuchi-hid-sync-failure] + supports: [ higuchi-hid-sync-failure ] contradicts: [] - id: unauthorized-power label: 私物機器の電源使用 description: 沢渡が許可なく私物機器を基地電源へ接続していたことが分かるが、解析室の事件とは無関係である。 reveal: - mode: conversation condition: 沢渡に基地電源へ私物機器を接続していなかったか尋ねたら開示する。 sources: - type: character id: sawatari - supports: [sawatari-used-extra-power] - contradicts: ["lie:sawatari-no-extra-power"] + supports: [ sawatari-used-extra-power ] + contradicts: [ "lie:sawatari-no-extra-power" ] solution: culprit: shinomiya summary: 犯人は篠宮怜。篠宮のアリバイを支える四つの時刻記録は独立していなかった。通信端末、入室履歴、自動観測ログ、壁時計はいずれも同じ中央時刻系を参照し、その中央時計は21時50分以降、実際より七分進んでいた。したがって四記録の一致は一つの誤差を共有した結果にすぎない。基地時刻と同期しない携帯時計を使う沢渡は、実際の22時20分に解析室側の通路で篠宮を目撃している。篠宮は22時23分ごろ解析室で牧瀬を襲った。牧瀬は篠宮による恣意的なデータ除外を翌朝の研究会議で取り上げる予定だった。複数証拠の数ではなく、それらの独立性を確かめることが解決の鍵となる。 method: 同じ中央時計を参照する複数の記録を独立した時刻証拠のように見せ、七分の同期ずれによって実際の行動時刻をずらして説明した motive: 恣意的なデータ除外を翌朝の研究会議で公にされ、研究成果と立場を失うことを恐れたため - requiredFacts: [central-clock-offset, four-times-not-independent, handheld-clock-correct, sawatari-saw-shinomiya-real-2220, makise-death-real-2223, disclosure-next-morning, shinomiya-killed-makise] secretKeywords: - 犯人は篠宮 - 篠宮が犯人 - 篠宮が牧瀬を襲 - 私が牧瀬を襲 - 四つの時計は同じ時刻源 -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [higuchi-hid-sync-failure, sawatari-used-extra-power] - notes: 複数時計で補強されたアリバイという古典的発想を、「証拠の相関」に置き換えたもの。四つの一致より一本の独立時計のほうが強い、という推理を要求する。 diff --git a/db/scenarios/quarantine-clinic-borrowed-badge.yaml b/db/scenarios/quarantine-clinic-borrowed-badge.yaml index 135914b..29cfa4c 100644 --- a/db/scenarios/quarantine-clinic-borrowed-badge.yaml +++ b/db/scenarios/quarantine-clinic-borrowed-badge.yaml @@ -1,15 +1,34 @@ schemaVersion: 1 id: quarantine-clinic-borrowed-badge meta: - title: 白嶺診療所、隔離の夜 + title: "白嶺診療所は本日休診" synopsis: "午後九時四十分、感染対策のため一時閉鎖中の白嶺診療所で、事務長の星名悟が事務室内で死亡しているのが見つかりました。外扉は午後八時以降封鎖され、館内にいたのは看護主任の世良美冬、検査技師の久我夏樹、警備担当の相原誠の三人だけです。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [診療所, 隔離, 人物識別, バッジ] victim: name: 星名悟 introduction: 白嶺診療所事務長 + foundAt: 21:40 + foundIn: 事務室 + estimatedDeathAt: "21:08" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 星名悟は事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「本部報告予定の確認メモ」に関わる資料が残されている。 +places: + - id: badge-reader + name: 検査廊下の認証端末 + shortName: 認証端末 + introduction: 防護区画を通る管理バッジの認証端末 + situation: 認証端末の画面が待機表示のまま残っている + findings: + - id: reader-records-badge + statement: 端末が保存しているのは通過したバッジ番号で、装着者の顔や氏名を記録する機能はない。 + - id: orange-badge-entry + statement: 21時18分には橙色の管理バッジが検査廊下を通過した記録が残っている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,11 +60,9 @@ facts: - id: kuga-borrowed-badge statement: 20時55分、久我夏樹は故障した自分の認証バッジの代わりに、星名悟から橙色の管理バッジを一時的に借りた kind: truth - secret: true - id: sera-approved-loan statement: 世良美冬は20時55分のバッジ貸与をその場で確認し、隔離区域の運用上問題ないと了承した kind: truth - secret: true - id: orange-badge-passed-2118 statement: 21時18分、橙色の管理バッジが検査廊下の認証地点を通過した kind: physical @@ -55,34 +72,27 @@ facts: - id: person-was-kuga-2118 statement: 21時18分に橙色の管理バッジを着けて検査廊下を通った人物は久我夏樹だった kind: truth - secret: true - id: hoshina-death-2108 statement: 21時08分ごろ、星名悟は事務室で襲われ死亡した kind: truth - secret: true - id: sera-killed-hoshina statement: 世良美冬は21時08分ごろ事務室で星名悟を襲い死亡させた kind: truth - secret: true - id: sera-claimed-hoshina-2118 statement: 世良美冬は21時18分の橙色バッジ通過を、星名悟がその時刻まで生きていた根拠として説明した kind: testimony - id: consent-audit-next-day statement: 星名悟は翌朝、世良美冬が研究参加同意書の管理手順を独断で変更していた件を本部へ報告する予定だった kind: motive - secret: true - id: sera-changed-consent-process statement: 世良美冬は業務を早めるため、研究参加同意書の確認手順を正式承認なしで変更していた kind: motive - secret: true - id: kuga-printed-private-results statement: 久我夏樹は業務外の個人的な検査結果を院内端末から印刷していた kind: other - secret: true - id: aihara-left-monitor statement: 相原誠は規定に反して数分間監視席を離れ、私用電話をしていた kind: other - secret: true - id: body-found-2140 statement: 21時40分、相原誠が事務室で星名悟の死を発見した kind: observation @@ -92,25 +102,29 @@ timeline: participants: [sera, kuga] facts: [kuga-borrowed-badge, sera-approved-loan] description: 久我が星名の橙色バッジを借り、世良もその貸与を確認する。 + location: 診療所内 - id: hoshina-death at: "21:08" participants: [sera] facts: [hoshina-death-2108, sera-killed-hoshina] description: 世良が事務室で星名を襲い、星名は死亡する。 + location: 事務室 - id: orange-passage at: "21:18" participants: [kuga, aihara] facts: [orange-badge-passed-2118, aihara-saw-orange-suit, person-was-kuga-2118] + record: 認証記録 description: 星名の橙色バッジを借りた久我が検査廊下を通り、相原がその姿を目撃する。 + location: 検査廊下 - id: discovery at: "21:40" participants: [aihara, sera, kuga] facts: [body-found-2140] description: 相原が事務室で星名の死を発見する。 + location: 事務室 characters: - id: sera name: 世良美冬 - role: suspect publicIntroduction: "冷静で判断が早い看護主任。" personality: 冷静で判断が早い看護主任。隔離手順を熟知し、自分の説明には必ず記録上の根拠を添える。業務効率のために手順を独断変更したことを星名に問題視されていた。 goals: @@ -137,10 +151,8 @@ characters: strategy: evasive memories: - id: badge-loan-memory - about: sera-approved-loan detail: 久我のバッジが反応せず、星名が自分の橙色バッジを貸した場面で「今夜だけなら」と自分が了承した。 - id: consent-warning - about: consent-audit-next-day detail: 星名から「明日は本部に正式報告する。善意でも手順を勝手に変えてはいけない」と告げられたことが頭から離れない。 relationships: - character: kuga @@ -151,7 +163,6 @@ characters: attitude: バッジの色だけで人を判断する癖があると知っている - id: kuga name: 久我夏樹 - role: witness publicIntroduction: "診療所の検査技師。" personality: 技術には詳しいが規則にはやや無頓着な検査技師。自分が管理バッジを借りたことを重大な規則違反だと思い込み、最初は隠そうとする。 goals: @@ -176,15 +187,15 @@ characters: strategy: maintain-until-contradicted memories: - id: orange-badge-corridor - about: person-was-kuga-2118 detail: 21時18分、橙色バッジを胸につけたまま検査廊下を通った。防護服姿なので相原には星名と間違われても不思議ではないと思った。 + - id: death-estimate-memory + detail: 検査担当として発見時の確認値を取り、星名の死亡は21時08分ごろと見積もられることを把握している。 relationships: - character: sera relation: 看護主任 attitude: バッジ貸与を了承したことまで忘れるはずがないと思っている - id: aihara name: 相原誠 - role: witness publicIntroduction: "診療所の警備担当。" personality: 真面目な警備員で、制服やバッジの色を人物識別に使う癖がある。防護服姿の顔は見分けられなかったことを認める一方、私用電話で席を外したことは隠したい。 goals: @@ -201,7 +212,6 @@ characters: strategy: maintain-until-contradicted memories: - id: badge-not-face - about: aihara-saw-orange-suit detail: 防護服とマスクで顔はほとんど見えず、橙色のバッジを見て星名だと思い込んだ。 relationships: - character: sera @@ -249,7 +259,6 @@ evidences: label: 一時バッジ貸与メモ description: 20時55分、久我の認証不良により星名の橙色バッジを一時貸与し、世良が了承したと記されている。 reveal: - mode: conversation condition: 久我か世良に20時55分ごろの認証バッジ不良について尋ね、貸与の有無を具体的に確認したら開示する。 sources: - type: character @@ -262,20 +271,19 @@ evidences: label: 二十一時十八分の認証記録 description: 記録されているのは橙色バッジの通過であり、使用者の顔や氏名を直接確認した記録ではない。 reveal: - mode: conversation - condition: 相原か久我に21時18分の認証記録が何を識別しているのか尋ねたら開示する。 + condition: 相原か久我に21時18分の認証記録が何を識別しているのか尋ねたら開示する。または検査廊下の認証端末を調べ、記録がバッジ番号だけで使用者を識別しないと確認したら開示する。 sources: - type: character id: aihara - type: character id: kuga + - { type: location, id: badge-reader } supports: [orange-badge-passed-2118, aihara-saw-orange-suit] contradicts: ["lie:sera-badge-proves-hoshina"] - id: kuga-corridor-admission label: 久我の検査廊下通過 description: 久我は星名の橙色バッジを着けたまま21時18分に検査廊下を通ったと認める。 reveal: - mode: conversation condition: 久我に橙色バッジを借りた後どこへ行ったか、21時18分の行動を時刻指定で追及したら開示する。 sources: - type: character @@ -286,20 +294,20 @@ evidences: label: 本部報告予定の確認メモ description: 星名の予定表に、翌朝最初の案件として世良による同意書管理手順の独断変更を本部へ報告すると記されている。 reveal: - mode: conversation - condition: 世良か久我に星名が翌朝予定していた本部報告の内容を尋ねたら開示する。 + condition: 世良か久我に星名が翌朝予定していた本部報告の内容を尋ねたら開示する。または遺体・現場を調べ、「本部報告予定の確認メモ」に関わる資料を確認したら開示する。 sources: - type: character id: sera - type: character id: kuga + - type: victim + id: victim supports: [sera-changed-consent-process, consent-audit-next-day] contradicts: [] - id: private-print-log label: 久我の私用印刷履歴 description: 久我が業務外の個人的な検査結果を印刷していたことが分かるが、星名の死とは独立した隠し事である。 reveal: - mode: conversation condition: 久我に検査室で業務外の印刷をしていなかったか追及したら開示する。 sources: - type: character @@ -310,30 +318,31 @@ evidences: label: 警備席の私用電話記録 description: 相原が数分間監視席を離れていたことが分かるが、21時18分の目撃自体はその前後に起きている。 reveal: - mode: conversation condition: 相原に監視席を離れた時間がなかったか具体的に尋ねたら開示する。 sources: - type: character id: aihara supports: [aihara-left-monitor] contradicts: ["lie:aihara-never-left"] + - id: death-estimate + label: 診療所の死亡推定 + description: 診療所の検査機器で発見時の状態を確認すると、星名の死亡は21時08分ごろと見積もられる。21時18分のバッジ通過より前である。 + reveal: + condition: 遺体を調べて検査機器の確認値を見るか、久我に発見時に取った検査値と死亡推定について尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: kuga } + supports: [hoshina-death-2108] + contradicts: [] + revealsDeathTime: true solution: culprit: sera summary: 犯人は世良美冬。21時18分の認証記録は星名本人の生存証明ではない。その時刻に橙色バッジを着けて検査廊下を通ったのは、20時55分に星名からバッジを借りた久我だった。防護服姿の顔を見分けられなかった相原は、バッジの色だけで星名だと思い込んだ。さらに世良はバッジ貸与をその場で了承しており、使用者が久我であり得ることを知りながら、21時18分の記録を星名の生存根拠として説明していた。星名は実際には21時08分ごろ事務室で世良に襲われている。翌朝、世良が独断変更した同意書管理手順が本部へ報告される予定だったことが動機となった。 method: 星名の管理バッジが一時的に久我へ貸与されていることを知りながら、その後のバッジ通過を星名本人の生存証明だと誤認させ、より早い時刻の犯行を隠した motive: 独断で変更した同意書管理手順を星名が翌朝本部へ正式報告する予定で、自分の責任問題になることを恐れたため - requiredFacts: [kuga-borrowed-badge, sera-approved-loan, person-was-kuga-2118, hoshina-death-2108, sera-claimed-hoshina-2118, consent-audit-next-day, sera-killed-hoshina] secretKeywords: - 犯人は世良 - 世良が犯人 - 世良が星名を襲 - 私が星名を襲 - バッジを生存証明に偽装 -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [kuga-printed-private-results, aihara-left-monitor] - notes: 人物と身につけている識別物を同一視する前提を崩すシナリオ。バッジ貸与の事実だけでなく、世良自身が貸与を知っていたことまで確認しないと単なる誤認で終わる。 diff --git a/db/scenarios/rainy-bookstore-receipt.yaml b/db/scenarios/rainy-bookstore-receipt.yaml index f537f49..fb8952d 100644 --- a/db/scenarios/rainy-bookstore-receipt.yaml +++ b/db/scenarios/rainy-bookstore-receipt.yaml @@ -1,15 +1,57 @@ schemaVersion: 1 id: rainy-bookstore-receipt meta: - title: 古書店青雨堂、雨の夜 - synopsis: "午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。" + title: "青雨堂、雨宿りの客" + synopsis: "午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後\ + 六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。" category: 日常系本格 difficulty: 2 estimatedMinutes: 10 - tags: [古書店, 雨, アリバイ] victim: name: 水野英治 introduction: 青雨堂店主 + foundAt: 19:15 + foundIn: 店奥 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 水野英治は店奥で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「店頭に残った複製本」に関わる資料が残されている。 +# 調べられる場所。喋らないが、聞き込みと同じ一手で調べる相手。 +# 図面を持たない事件なので、ID は部屋を指さない。ここで名付けた ID が +# `type: location` のソースの行き先になる。 +places: + - id: choba + name: 帳場 + shortName: 帳場 + introduction: 青雨堂の一階。レジと帳面 + # 調べているあいだ名札の下に出る一行。所見ではなく、見れば誰でも分かる佇まい。 + situation: 閉店の片づけが、途中で止まっている + # 主語は場所ではなく「分かったこと」。誰がそうしたのかは書かない。 + findings: + - id: ledger-stops-1844 + statement: 帳場の帳面は18時44分の記入で止まっていて、その先が書かれていない。 + - id: closing-unfinished + statement: 釣り銭は数えかけのまま、戸締まりもされていない。片づけが途中で放り出されている。 + - id: ledger-loan-note + statement: 帳面の裏表紙に、貸し借りらしい数字の覚え書きがある。相手の名は略字で書かれている。 + - id: oku + name: 奥の間 + shortName: 奥の間 + introduction: 帳場の裏。倒れていた場所 + situation: 書架のあいだに、灯りがひとつだけ点いている + findings: + - id: shelf-gap + statement: 詩集の棚に一冊ぶんの隙間があり、抜かれた本が戻されていない。 + - id: single-lamp + statement: 書架のあいだの灯りがひとつだけ点いている。手元を照らすためだけに点けたように見える。 + # 複製本の一件を掴んでから見ると、棚の隙間の意味が変わる。順序を作るために前提を置く。 + - id: replica-shelf-mark + statement: 隙間の奥の埃に、本を一度戻して抜き直したような跡が二重に残っている。 + requires: + evidences: + - forged-book briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +83,12 @@ facts: - id: makino-swapped-book statement: 牧野千尋は数日前、その初版本を精巧な複製本とすり替え、本物を転売する準備をしていた kind: motive - secret: true - id: mizuno-discovered-swap statement: 18時37分ごろ、水野英治は初版本のすり替えに気づき、牧野千尋を問い詰めた kind: motive - secret: true - id: kuroda-secret-offer statement: 黒田征司は18時28分ごろ、水野英治に帳簿へ残さない現金取引で初版本を売ってほしいと持ちかけた kind: motive - secret: true - id: kuroda-left-1842 statement: 18時42分、黒田征司は青雨堂を出たが、雨が強くなったため商店街の軒下でしばらく雨宿りした kind: observation @@ -59,18 +98,15 @@ facts: - id: makino-killed-mizuno-1850 statement: 18時50分ごろ、牧野千尋は店の奥で水野英治を襲い死亡させた kind: truth - secret: true - id: makino-left-1856 statement: 18時56分ごろ、牧野千尋は発送用の小包を持って青雨堂を出た kind: observation - secret: true - id: post-receipt-1908 statement: 19時08分、牧野千尋は商店街の郵便窓口で小包を発送した kind: physical - id: sena-owed-money statement: 瀬名真琴は店の改装費として水野英治から借りた百万円を返済できず、返済期限を延ばしてもらっていた kind: motive - secret: true - id: sena-saw-makino-leave statement: 18時56分ごろ、瀬名真琴は喫茶店の窓から、小包を持って青雨堂を出る牧野千尋を見た kind: observation @@ -80,58 +116,79 @@ facts: - id: genuine-book-in-locker statement: すり替えられた本物の初版本は、牧野千尋が利用していた駅の貸しロッカーから後に見つかった kind: physical - secret: true timeline: - id: kuroda-offer at: "18:28" - participants: [kuroda] - facts: [kuroda-secret-offer] + participants: [ kuroda ] + facts: [ kuroda-secret-offer ] description: 黒田が水野に、記録へ残さない現金取引を持ちかける。 + location: 店内 - id: swap-discovered at: "18:37" - participants: [makino] - facts: [mizuno-discovered-swap] + participants: [ makino ] + facts: [ mizuno-discovered-swap ] description: 水野が初版本のすり替えに気づき、牧野を問い詰める。 + location: 店内 - id: kuroda-leaves at: "18:42" - participants: [kuroda] - facts: [kuroda-left-1842] + participants: [ kuroda ] + facts: [ kuroda-left-1842 ] description: 黒田が店を出るが、激しい雨のため近くの軒下で雨宿りする。 + location: 軒下 - id: kuroda-sighting at: "18:47" - participants: [kuroda, makino] - facts: [kuroda-saw-makino-1847] + participants: [ makino ] + witnesses: [ kuroda ] + facts: [ kuroda-saw-makino-1847 ] description: 黒田が正面ガラス越しに、まだ店内にいる牧野を目撃する。 + location: 店内 + - id: kuroda-under-eaves + at: "18:47" + participants: [ kuroda ] + facts: [ kuroda-saw-makino-1847 ] + description: 黒田は軒下で雨宿りを続け、正面ガラス越しに店内の牧野を見る。 + location: 軒下 - id: mizuno-death at: "18:50" - participants: [makino] - facts: [makino-killed-mizuno-1850] + participants: [ makino ] + facts: [ makino-killed-mizuno-1850 ] description: 牧野が店の奥で水野を襲い、水野は死亡する。 + location: 店奥 - id: makino-departs at: "18:56" - participants: [makino, sena] - facts: [makino-left-1856, sena-saw-makino-leave] + participants: [ makino ] + witnesses: [ sena ] + facts: [ makino-left-1856, sena-saw-makino-leave ] description: 牧野が小包を持って店を出るところを、向かいの喫茶店にいた瀬名が見る。 + location: 店先 + - id: sena-in-cafe + at: "18:56" + participants: [ sena ] + facts: [ sena-saw-makino-leave ] + description: 瀬名は向かいの喫茶店から、牧野が青雨堂を出るところを見る。 + location: 向かいの喫茶 - id: parcel-posted at: "19:08" - participants: [makino] - facts: [post-receipt-1908] + participants: [ makino ] + facts: [ post-receipt-1908 ] + record: 受付 description: 牧野が郵便窓口で小包を発送し、時刻入りのレシートを受け取る。 + location: 郵便窓口 - id: discovery at: "19:15" - participants: [sena] - facts: [body-found-1915] + participants: [ sena ] + facts: [ body-found-1915 ] description: 瀬名が青雨堂を訪ね、店の奥で水野の死を発見する。 + location: 店奥 characters: - id: makino name: 牧野千尋 - role: suspect publicIntroduction: "丁寧で几帳面な古書店員。" personality: 丁寧で几帳面な古書店員。書誌や発送手順には強いが、金銭の話を向けられると防御的になる。水野には仕事を教わった恩がある一方、給料の低さには不満を抱えていた。 goals: - 初版本のすり替えを隠したい - 18時35分以降は郵便局へ向かっていたというアリバイを維持したい - knowledge: [makino-is-clerk, rare-book-arrived, post-receipt-1908, body-found-1915] + knowledge: [ makino-is-clerk, rare-book-arrived, post-receipt-1908, body-found-1915 ] secrets: - fact: makino-swapped-book disclosure: never @@ -150,7 +207,6 @@ characters: strategy: maintain-until-contradicted memories: - id: caught-swap - about: mizuno-discovered-swap detail: 複製本を手にした水野から「これは本物じゃないな」と静かに言われた瞬間、頭の中が真っ白になった。 relationships: - character: kuroda @@ -158,13 +214,19 @@ characters: attitude: 値切り方が強引で苦手 - id: kuroda name: 黒田征司 - role: suspect publicIntroduction: "希少本の知識を誇る収集家。" personality: 希少本の知識を誇る収集家。金で解決できると思いがちで、体面を傷つけられることを嫌う。事件そのものより、裏取引の提案が会社へ知られることを恐れている。 goals: - 帳簿外の現金取引を持ちかけたことを隠したい - 事件への関与を疑われないよう、店の奥へ入っていないと強調したい - knowledge: [kuroda-is-collector, rare-book-arrived, kuroda-left-1842, kuroda-saw-makino-1847, body-found-1915] + knowledge: + [ + kuroda-is-collector, + rare-book-arrived, + kuroda-left-1842, + kuroda-saw-makino-1847, + body-found-1915 + ] secrets: - fact: kuroda-secret-offer disclosure: pressured @@ -175,7 +237,6 @@ characters: strategy: maintain-until-contradicted memories: - id: rain-window-sighting - about: kuroda-saw-makino-1847 detail: 雨宿りしながら何気なく店を見たら、牧野がカウンター奥を早足で横切ったのを覚えている。 relationships: - character: makino @@ -183,13 +244,12 @@ characters: attitude: 本を見る目は信用している - id: sena name: 瀬名真琴 - role: witness publicIntroduction: "気さくな喫茶店主で、商店街の人間関係には詳しい。" personality: 気さくな喫茶店主で、商店街の人間関係には詳しい。借金のことだけは強い引け目があり、水野との金銭関係を聞かれると曖昧になる。 goals: - 水野から借金していたことを知られたくない - 窓から見た18時56分の出来事は正確に伝えたい - knowledge: [sena-runs-cafe, sena-saw-makino-leave, body-found-1915] + knowledge: [ sena-runs-cafe, sena-saw-makino-leave, body-found-1915 ] secrets: - fact: sena-owed-money disclosure: pressured @@ -200,7 +260,6 @@ characters: strategy: maintain-until-contradicted memories: - id: parcel-in-rain - about: sena-saw-makino-leave detail: 雨の向こうで、牧野が小包を胸に抱えて店から飛び出していく姿が妙に印象に残っている。 relationships: [] revelations: @@ -217,8 +276,8 @@ revelations: revealCondition: 牧野に郵便局へ着いた時刻とレシートが示す時刻の違いを具体的に確認した。 requires: revelations: [] - evidences: [postal-receipt] - relatedFacts: [post-receipt-1908, makino-left-1856] + evidences: [ postal-receipt ] + relatedFacts: [ post-receipt-1908, makino-left-1856 ] - id: swap-motive title: 初版本のすり替え text: 牧野は初版本を複製本とすり替えており、水野は事件直前にその不正を見抜いていた。 @@ -231,95 +290,86 @@ revelations: id: makino revealCondition: 牧野に初版本の真贋と水野に問い詰められた理由を追及し、すり替えの存在が明確になった。 requires: - revelations: [post-receipt-is-not-alibi] - evidences: [forged-book] - relatedFacts: [makino-swapped-book, mizuno-discovered-swap, genuine-book-in-locker] + revelations: [ post-receipt-is-not-alibi ] + evidences: [ forged-book ] + relatedFacts: [ makino-swapped-book, mizuno-discovered-swap, genuine-book-in-locker ] evidences: - id: postal-receipt label: 郵便窓口の十九時八分のレシート description: 小包の受付時刻は19時08分。18時台に牧野が窓口にいたことを示す記録はない。 reveal: - mode: conversation condition: 牧野に発送時刻や郵便局で待っていた時間を詳しく尋ね、アリバイの根拠としてレシートを示したら開示する。 sources: - type: character id: makino - supports: [post-receipt-1908] + supports: [ post-receipt-1908 ] contradicts: [] - id: window-sighting label: 雨宿り中の黒田の目撃 description: 黒田は18時47分ごろ、青雨堂の正面ガラス越しに牧野が店内にいるのを見ている。 reveal: - mode: conversation condition: 黒田に店を出た後どこで雨宿りし、店内が見えたかを尋ね、18時47分の牧野の姿を話したら開示する。 sources: - type: character id: kuroda - supports: [kuroda-saw-makino-1847] - contradicts: ["lie:makino-post-office-alibi"] + supports: [ kuroda-saw-makino-1847 ] + contradicts: [ "lie:makino-post-office-alibi" ] - id: cafe-sighting label: 十八時五十六分の喫茶店からの目撃 description: 瀬名は18時56分ごろ、小包を持って青雨堂から出る牧野を見ている。 reveal: - mode: conversation condition: 瀬名に青雨堂の出入口を見ていた時間帯と、店から出た人物について尋ねたら開示する。 sources: - type: character id: sena - supports: [sena-saw-makino-leave, makino-left-1856] - contradicts: ["lie:makino-post-office-alibi"] + supports: [ sena-saw-makino-leave, makino-left-1856 ] + contradicts: [ "lie:makino-post-office-alibi" ] - id: forged-book label: 店頭に残った複製本 description: 初版本として保管されていた本は精巧な複製で、在庫と発送を扱う者ならすり替えの機会があった。 reveal: - mode: conversation - condition: 牧野か黒田に事件当日の高価な初版本について詳しく尋ね、真贋に疑問が出たら開示する。 + condition: 牧野か黒田に事件当日の高価な初版本について詳しく尋ね、真贋に疑問が出たら開示する。または遺体・現場を調べ、「店頭に残った複製本」に関わる資料を確認したら開示する。または奥の間の棚を調べ、抜かれたまま戻っていない一冊の跡に行き当たったら開示する。 sources: - type: character id: makino - type: character id: kuroda - supports: [makino-swapped-book, rare-book-arrived] + - type: victim + id: victim + - type: location + id: oku + supports: [ makino-swapped-book, rare-book-arrived ] contradicts: [] - id: secret-deal-note label: 黒田の現金取引メモ description: 黒田が帳簿外の現金取引を提案した金額のメモが残るが、事件時刻の行動とは結びつかない。 reveal: - mode: conversation condition: 黒田に水野との商談条件を繰り返し確認し、通常の購入相談ではなかった可能性を追及したら開示する。 sources: - type: character id: kuroda - supports: [kuroda-secret-offer] - contradicts: ["lie:kuroda-no-secret-deal"] + supports: [ kuroda-secret-offer ] + contradicts: [ "lie:kuroda-no-secret-deal" ] - id: debt-ledger label: 瀬名への貸付記録 description: 水野が瀬名へ百万円を貸していた記録。返済は遅れているが、強い取り立てをしていた形跡はない。 reveal: - mode: conversation - condition: 瀬名に水野との金銭関係を尋ね、貸し借りを否定したため記録を確認したら開示する。 + condition: 瀬名に水野との金銭関係を尋ね、貸し借りを否定したため記録を確認したら開示する。または帳場の帳面を調べ、貸し借りの覚え書きに行き当たったら開示する。 sources: - type: character id: sena - supports: [sena-owed-money] - contradicts: ["lie:sena-no-debt"] + - type: location + id: choba + supports: [ sena-owed-money ] + contradicts: [ "lie:sena-no-debt" ] solution: culprit: makino summary: 犯人は牧野千尋。高額な初版本を複製本とすり替えて本物を転売しようとしていたが、18時37分ごろ水野に見抜かれた。牧野は18時50分ごろ店の奥で水野を襲い、18時56分に発送用の小包を持って店を出た。その後19時08分に郵便窓口で発送し、このレシートを「18時35分から郵便局にいた」証拠のように見せた。しかし黒田は18時47分に店内の牧野を見ており、瀬名も18時56分に店を出る牧野を目撃している。二つの独立した目撃が牧野のアリバイと衝突する。黒田の裏取引提案と瀬名の借金は疑わしいが、殺害時刻の矛盾には繋がらない。 method: 閉店後の店内で水野を襲い、殺害後に小包を発送して郵便局の時刻をアリバイへ利用した motive: 初版本のすり替えが発覚し、警察と業界へ知られることを恐れたため - requiredFacts: [makino-swapped-book, mizuno-discovered-swap, kuroda-saw-makino-1847, makino-killed-mizuno-1850, makino-left-1856, sena-saw-makino-leave, post-receipt-1908] secretKeywords: - 犯人は牧野 - 牧野が犯人 - 牧野が水野を襲 - 私が水野を襲 - すり替え発覚で殺 -quality: - expectedQuestionCount: - min: 8 - max: 18 - requiredEvidence: - min: 2 - redHerrings: [kuroda-secret-offer, sena-owed-money] - notes: レシートを時刻の点の証拠として扱わせ、そこから「待っていた時間」まで勝手に拡張しないことが核心。黒田と瀬名の別方向の目撃を用意し、どちらか一人への質問を逃しても牧野の嘘へ到達できる。 diff --git a/db/scenarios/shanghai-warehouse-carbon-copy.yaml b/db/scenarios/shanghai-warehouse-carbon-copy.yaml index f2aa6b1..b0d453c 100644 --- a/db/scenarios/shanghai-warehouse-carbon-copy.yaml +++ b/db/scenarios/shanghai-warehouse-carbon-copy.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: shanghai-warehouse-carbon-copy meta: - title: 1928年上海、河岸倉庫の夜 + title: "上海、雨は倉庫街に降る" synopsis: "1928年、上海。暴風雨で河岸の道路が封鎖された夜、輸出倉庫の事務室で貿易商・周文海が死亡しているのが見つかりました。" category: 歴史クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [1928年, 上海, 倉庫, 複写伝票] victim: name: 周文海 introduction: 上海の貿易商 + foundAt: 22:20 + foundIn: 事務室 + estimatedDeathAt: "21:52" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 周文海は事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「絹荷の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -47,34 +55,27 @@ facts: - id: lin-smuggled-silk statement: 林雪梅は帳簿外の絹荷を倉庫から流していた kind: motive - secret: true - id: zhou-found-smuggling statement: 周文海は事件当日、林雪梅による帳簿外取引に気づいた kind: motive - secret: true - id: zhou-signed-blank-form statement: 21時35分ごろ、周文海は翌朝搬出予定の荷のため時刻欄が空白の伝票へ先に署名した kind: truth - secret: true - id: chen-saw-blank-time statement: 陳伯安は21時40分ごろ、周の署名済み伝票の時刻欄が空白だったことを見ている kind: observation - id: lin-killed-zhou statement: 21時52分ごろ、林雪梅は事務室で周文海を襲い死亡させた kind: truth - secret: true - id: lin-added-2205 statement: 22時05分ごろ、林雪梅は署名済み伝票の上紙にだけ時刻を書き足した kind: truth - secret: true - id: chen-bribed-inspector statement: 陳伯安は別件の通関を早めるため役人へ不正な謝礼を渡していた kind: other - secret: true - id: wang-stole-cargo statement: 王世傑は破損扱いにした輸入品を少量持ち出していた kind: other - secret: true - id: body-found-2220 statement: 22時20分、王世傑が事務室で周文海の死を発見した kind: observation @@ -84,30 +85,35 @@ timeline: participants: [lin, chen] facts: [zhou-signed-blank-form] description: 周が翌朝の搬出用に、時刻欄が空白の伝票へ先に署名する。 + location: 事務室 - id: chen-sees-form at: "21:40" participants: [chen] facts: [chen-saw-blank-time] description: 陳が署名済み伝票を確認し、時刻欄がまだ空白なのを見る。 + location: 事務室 - id: zhou-death at: "21:52" participants: [lin] facts: [lin-killed-zhou, lin-smuggled-silk, zhou-found-smuggling] description: 林が帳簿外取引の発覚を恐れ、事務室で周を襲う。 + location: 事務室 - id: time-added at: "22:05" participants: [lin] facts: [lin-added-2205, top-sheet-time-added-later] + record: 搬出伝票 description: 林が署名済み伝票の上紙だけに22時05分と書き足す。 + location: 事務室 - id: discovery at: "22:20" participants: [lin, chen, wang] facts: [body-found-2220] description: 王が事務室で周の死を発見する。 + location: 事務室 characters: - id: lin name: 林雪梅 - role: suspect publicIntroduction: "冷静で計算高い番頭。" personality: 冷静で計算高い番頭。書類の扱いに慣れており、署名入り伝票を強い生存証明として押し出そうとする。帳簿外の荷について問われると警戒する。 goals: @@ -134,7 +140,6 @@ characters: strategy: maintain-until-contradicted memories: - id: lin-caught - about: zhou-found-smuggling detail: 周に帳簿を閉じられ「明朝、全部数え直す」と言われたとき、終わったと思った。 relationships: - character: chen @@ -145,7 +150,6 @@ characters: attitude: 荷の出入りを知りすぎているので信用していない - id: chen name: 陳伯安 - role: suspect publicIntroduction: "細部に強い通関書記。" personality: 細部に強い通関書記。紙の筆圧や控えの違いに敏感だが、自分の不正な謝礼については口を濁す。 goals: @@ -162,8 +166,9 @@ characters: strategy: maintain-until-contradicted memories: - id: chen-blank-box - about: chen-saw-blank-time detail: 周の署名はあったのに、時刻の四角だけ白いままだったのが妙に印象に残っている。 + - id: death-estimate-memory + detail: 発見後の検視内容を業務メモへ写しており、周の死亡は21時52分ごろと見積もられていたことを覚えている。 relationships: - character: lin relation: 上司 @@ -173,7 +178,6 @@ characters: attitude: 荷の扱いは荒いが目撃は率直だと思っている - id: wang name: 王世傑 - role: witness publicIntroduction: "豪胆な荷役監督。" personality: 豪胆な荷役監督。細かな帳簿は苦手だが、物品の状態には目が利く。破損品の持ち出しだけは隠したい。 goals: @@ -190,7 +194,6 @@ characters: strategy: maintain-until-contradicted memories: - id: wang-carbon-difference - about: top-sheet-time-added-later detail: 三枚を揃えたとき、時刻だけ下の控えに写っていないのが変だと思った。 relationships: - character: lin @@ -231,7 +234,6 @@ evidences: label: 三枚綴りの搬出伝票 description: 署名は三枚すべてに複写されているが、22時05分という時刻は上紙にしか存在しない。 reveal: - mode: conversation condition: 陳か王に搬出伝票の三枚を比較した違いを尋ねたら開示する。 sources: - { type: character, id: chen } @@ -242,7 +244,6 @@ evidences: label: 21時40分の書記メモ description: 陳の作業メモには署名済み伝票の番号と「時刻未記入」と残っている。 reveal: - mode: conversation condition: 陳に署名済み伝票をいつ確認したか尋ね、作業メモを確認したら開示する。 sources: - { type: character, id: chen } @@ -252,36 +253,40 @@ evidences: label: 絹荷の不一致 description: 倉庫の実数と帳簿が合わず、周が林の担当欄に翌朝再検査の印を付けている。 reveal: - mode: conversation - condition: 林か陳に周が事件前に調べていた荷の不一致を尋ねたら開示する。 + condition: 林か陳に周が事件前に調べていた荷の不一致を尋ねたら開示する。または遺体・現場を調べ、「絹荷の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: lin } - { type: character, id: chen } + - { type: victim, id: victim } supports: [lin-smuggled-silk, zhou-found-smuggling] contradicts: ["lie:lin-no-smuggling"] - id: broken-cargo-cache label: 隠された破損品 description: 王が持ち出そうとしていた破損品が見つかるが、事務室の事件とは独立している。 reveal: - mode: conversation condition: 王に破損扱いの荷が帳簿と合わない理由を追及したら開示する。 sources: - { type: character, id: wang } supports: [wang-stole-cargo] contradicts: ["lie:wang-no-cargo-theft"] + - id: death-estimate + label: 当夜の検視メモ + description: 発見後に作られた検視メモでは、周の死亡は21時52分ごろと見積もられている。上紙へ22時05分が書き足されるより前である。 + reveal: + condition: 遺体を調べて当夜の検視内容を確認するか、陳に発見後に控えた検視メモの時刻を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: chen } + supports: [lin-killed-zhou] + contradicts: [] + revealsDeathTime: true solution: culprit: lin summary: 犯人は林雪梅。帳簿外の絹荷を周に見抜かれ、21時52分ごろ事務室で周を襲った。周はその前に翌朝の搬出用として時刻欄が空白の伝票へ署名していたため、林は22時05分になって上紙へ時刻だけを書き足し、周がその時刻まで生きていたように見せた。だが三枚綴りの複写伝票では、同時に書かれた文字なら控えにも写る。署名は三枚にあるのに時刻は上紙だけで、陳も21時40分に時刻欄が空白だったと記録している。 method: 事件前に署名された複写伝票の上紙へ後から時刻だけを書き足し、被害者の生存時刻を偽装した motive: 帳簿外の絹荷取引が発覚するのを防ぐため - requiredFacts: [three-part-carbon-form, top-sheet-time-added-later, signature-on-all-three, lin-smuggled-silk, zhou-signed-blank-form, chen-saw-blank-time, lin-killed-zhou, lin-added-2205] secretKeywords: - 犯人は林 - 林が周を襲 - 林が時刻を書き足 - 私が時刻を書き足 -quality: - expectedQuestionCount: { min: 10, max: 22 } - requiredEvidence: { min: 3 } - redHerrings: [chen-bribed-inspector, wang-stole-cargo] - notes: 署名と時刻が同時に書かれたという前提を、複写の物理と書記メモの二方向から崩す。 diff --git a/db/scenarios/snow-gallery-erased-tracks.yaml b/db/scenarios/snow-gallery-erased-tracks.yaml index 82c0277..7a003c9 100644 --- a/db/scenarios/snow-gallery-erased-tracks.yaml +++ b/db/scenarios/snow-gallery-erased-tracks.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: snow-gallery-erased-tracks meta: - title: 白庭彫刻館、雪の夜 + title: "彫刻は雪を見ている" synopsis: "午後八時五十分、吹雪で山道が閉ざされた白庭彫刻館の離れ展示室で、館長の青沼卓が死亡しているのが見つかりました。館内に残っていたのは学芸員の倉田真帆、施設員の安西雄、彫刻家の江波涼の三人だけです。" category: クローズドサークル difficulty: 4 estimatedMinutes: 15 - tags: [雪, 美術館, 足跡, 時系列] victim: name: 青沼卓 introduction: 白庭彫刻館館長 + foundAt: 20:50 + foundIn: 離れ展示室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 青沼卓は離れ展示室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「来歴記録の修正履歴」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +48,12 @@ facts: - id: kurata-crossed-2017 statement: 20時17分ごろ、倉田真帆は母屋から離れ展示室へ向かった kind: truth - secret: true - id: aoonuma-death-2020 statement: 20時20分ごろ、青沼卓は離れ展示室で襲われ死亡した kind: truth - secret: true - id: kurata-killed-aoonuma statement: 倉田真帆は20時20分ごろ離れ展示室で青沼卓を襲い死亡させた kind: truth - secret: true - id: anzai-cleared-path-2026 statement: 20時26分ごろ、安西雄は排水口確認のため屋外通路の積雪を入口から離れまで一度掃き直した kind: observation @@ -65,19 +69,15 @@ facts: - id: provenance-forgery-found statement: 青沼卓は倉田真帆が一部作品の来歴記録を都合よく書き換えていたことに気づいた kind: motive - secret: true - id: board-report-next-day statement: 青沼卓は翌日の理事会で倉田真帆による来歴記録の書き換えを報告する予定だった kind: motive - secret: true - id: anzai-broke-sensor statement: 安西雄は作業中に雪害センサーを壊し、自然故障として処理しようとしていた kind: other - secret: true - id: enami-damaged-model statement: 江波涼は展示前の試作品を誤って破損し、青沼に報告していなかった kind: other - secret: true - id: body-found-2050 statement: 20時50分、江波涼が離れ展示室で青沼卓の死を発見した kind: observation @@ -86,36 +86,44 @@ timeline: at: "20:10" participants: [] facts: [snow-started-2010] + record: 積雪層 description: 母屋と離れの間の通路に雪が積もり始める。 + location: 母屋 - id: kurata-crosses at: "20:17" participants: [kurata] facts: [kurata-crossed-2017] description: 倉田が母屋から離れ展示室へ向かう。 + location: 離れ展示室 - id: aoonuma-death at: "20:20" participants: [kurata] facts: [aoonuma-death-2020, kurata-killed-aoonuma] description: 倉田が離れ展示室で青沼を襲い、青沼は死亡する。 + location: 離れ展示室 - id: path-cleared at: "20:26" participants: [anzai] facts: [anzai-cleared-path-2026, snow-covered-after-clearing] + record: 除雪作業記録 description: 安西が排水口確認のため屋外通路を一度掃き直し、それ以前の足跡も雪面ごと崩れる。 + location: 屋外通路 - id: kurata-back-main at: "20:31" participants: [kurata, anzai] facts: [anzai-saw-kurata-2031] description: 安西が母屋裏口付近にいる倉田を目撃する。 + location: 母屋裏口 - id: discovery at: "20:50" participants: [enami, kurata, anzai] facts: [body-found-2050, no-tracks-at-discovery] + record: 無傷の雪面 description: 江波が離れで青沼の死を発見し、通路には判別できる足跡が残っていない。 + location: 離れ展示室 characters: - id: kurata name: 倉田真帆 - role: suspect publicIntroduction: "彫刻館の学芸員。" personality: 落ち着いた学芸員で、展示記録や時系列を細かく説明する。雪面の状態を物理的証拠として強調する一方、来歴記録の問題を青沼に見つけられていた。 goals: @@ -138,10 +146,8 @@ characters: strategy: maintain-until-contradicted memories: - id: clearing-saw-from-window - about: anzai-cleared-path-2026 detail: 安西が通路の雪を掃いているのを母屋の窓越しに見て、自分の足跡が残らないと気づいた。 - id: board-fear - about: board-report-next-day detail: 青沼から「明日の理事会には修正履歴も全部出す」と告げられ、職を失うと感じた。 relationships: - character: anzai @@ -152,7 +158,6 @@ characters: attitude: 作品以外には無頓着だと思っている - id: anzai name: 安西雄 - role: witness publicIntroduction: "彫刻館の施設員。" personality: 無愛想だが作業時刻には几帳面な施設員。雪害センサーを壊した失敗を隠したい。通路を掃いたことで、発見時の雪面が犯行時の雪面ではないことを知っている。 goals: @@ -169,10 +174,8 @@ characters: strategy: maintain-until-contradicted memories: - id: clearing-time - about: anzai-cleared-path-2026 detail: 排水口が詰まりかけたので20時26分から数分、通路の積雪を掃いた。その後すぐ新しい雪が積もり始めた。 - id: kurata-back-door - about: anzai-saw-kurata-2031 detail: 除雪を終えて戻った直後、母屋裏口に倉田がいたので「外に出ていたのか」と思った。 relationships: - character: kurata @@ -180,7 +183,6 @@ characters: attitude: 雪のことを知ったように断言するのが気になる - id: enami name: 江波涼 - role: witness publicIntroduction: "感覚派の彫刻家。" personality: 感覚派の彫刻家。時刻には曖昧だが展示室の状態にはよく気づく。試作品の破損を隠しているため、青沼との口論を知られたくない。 goals: @@ -197,7 +199,6 @@ characters: strategy: maintain-until-contradicted memories: - id: discovery-snow - about: no-tracks-at-discovery detail: 離れへ向かったとき雪面がきれいだったので、最初は本当に誰も来ていないと思った。 relationships: - character: kurata @@ -245,7 +246,6 @@ evidences: label: 二十時二十六分の除雪作業記録 description: 安西が排水口確認のため母屋から離れまで通路の雪を一度掃き直したことが記録されている。 reveal: - mode: conversation condition: 安西に20時台の屋外作業と排水口確認について尋ねたら開示する。 sources: - type: character @@ -256,7 +256,6 @@ evidences: label: 二十時三十一分の裏口目撃 description: 安西は除雪を終えた直後、母屋の裏口付近にいる倉田を見ている。 reveal: - mode: conversation condition: 安西に除雪作業を終えて母屋へ戻ったとき誰を見たか尋ねたら開示する。 sources: - type: character @@ -267,7 +266,6 @@ evidences: label: 通路の積雪層メモ description: 発見時の表面は20時26分の除雪後に積もった雪で、それ以前の足跡を保存していないことが分かる。 reveal: - mode: conversation condition: 安西か江波に発見時の雪面と20時26分の除雪の前後関係を確認したら開示する。 sources: - type: character @@ -280,20 +278,20 @@ evidences: label: 来歴記録の修正履歴 description: 倉田が担当した複数作品で、根拠資料と合わない来歴修正が行われ、青沼が翌日理事会へ報告する印を付けている。 reveal: - mode: conversation - condition: 倉田か江波に青沼が直前まで確認していた作品来歴について尋ねたら開示する。 + condition: 倉田か江波に青沼が直前まで確認していた作品来歴について尋ねたら開示する。または遺体・現場を調べ、「来歴記録の修正履歴」に関わる資料を確認したら開示する。 sources: - type: character id: kurata - type: character id: enami + - type: victim + id: victim supports: [provenance-forgery-found, board-report-next-day] contradicts: [] - id: broken-snow-sensor label: 破損した雪害センサー description: 安西の作業ミスでセンサーが壊れたことが分かるが、青沼の死とは独立した隠し事である。 reveal: - mode: conversation condition: 安西に雪害センサーの故障原因を追及したら開示する。 sources: - type: character @@ -304,7 +302,6 @@ evidences: label: 破損した試作品 description: 江波が展示前の試作品を壊して隠していたことが分かるが、離れ展示室の事件とは無関係である。 reveal: - mode: conversation condition: 江波に展示前の試作品の破損について尋ねたら開示する。 sources: - type: character @@ -316,18 +313,9 @@ solution: summary: 犯人は倉田真帆。発見時に足跡がなかったことは、20時10分の降雪開始以降ずっと誰も離れへ渡っていない証明ではない。倉田は20時17分ごろ離れへ渡り、20時20分ごろ青沼を襲った。その後20時26分、安西が排水口確認のため通路の雪を一度掃き直したため、それ以前の足跡は雪面ごと消えた。さらに降雪が続き、発見時には除雪後の通路にも新しい雪が積もっていた。20時31分には安西が母屋裏口で倉田を目撃している。青沼は倉田による作品来歴記録の書き換えを翌日の理事会で報告する予定だった。無足跡という証拠の「保存開始時刻」を見誤らないことが核心である。 method: 降雪中に離れへ渡った後、施設員の除雪作業によってそれ以前の足跡が失われたことを利用し、発見時の無傷の雪面を自分の不在証明として主張した motive: 作品来歴記録の不適切な書き換えを翌日の理事会で公にされ、学芸員としての信用と職を失うことを恐れたため - requiredFacts: [kurata-crossed-2017, anzai-cleared-path-2026, snow-covered-after-clearing, anzai-saw-kurata-2031, provenance-forgery-found, board-report-next-day, kurata-killed-aoonuma] secretKeywords: - 犯人は倉田 - 倉田が犯人 - 倉田が青沼を襲 - 私が青沼を襲 - 除雪で足跡が消え -quality: - expectedQuestionCount: - min: 11 - max: 22 - requiredEvidence: - min: 3 - redHerrings: [anzai-broke-sensor, enami-damaged-model] - notes: 古典的な無足跡問題を、特殊な移動手段ではなく証拠保存の時間窓で解く。発見時の状態が犯行時から連続していたという思い込みを崩す。 diff --git a/db/scenarios/snow-observatory-last-photo.yaml b/db/scenarios/snow-observatory-last-photo.yaml index 65794dc..d85d356 100644 --- a/db/scenarios/snow-observatory-last-photo.yaml +++ b/db/scenarios/snow-observatory-last-photo.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: snow-observatory-last-photo meta: - title: 北岳観測所、吹雪の夜 + title: "北岳観測所殺人事件" synopsis: "午後十時十五分、北岳観測所の資料室で、所長の神崎遼が死亡しているのが見つかりました。外は激しい吹雪で、午後九時以降に天文台を出入りした者はいません。" category: クローズドサークル difficulty: 3 estimatedMinutes: 10 - tags: [天文台, 雪, 写真] victim: name: 神崎遼 introduction: 北岳観測所所長 + foundAt: 22:15 + foundIn: 資料室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 神崎遼は資料室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「神崎の観測データ検証メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,103 +45,107 @@ facts: - id: hiyama-fabricated-data statement: 日山澪は論文の結論を強めるため、一部の観測データを不正に補正していた kind: motive - secret: true - id: kanzaki-found-fabrication statement: 神崎遼は事件当日、日山澪による観測データの不正補正に気づいた kind: motive - secret: true - id: kanzaki-would-retract statement: 21時40分ごろ、神崎遼は日山澪に翌朝論文を撤回し、大学へ不正を報告すると告げた kind: motive - secret: true - id: kurose-copied-data statement: 黒瀬俊介は共同研究の未公開データを無断で複製し、自分の研究室へ持ち帰ろうとしていた kind: other - secret: true - id: camera-interval-started statement: 21時50分、日山澪は屋上のカメラを三分間隔の自動撮影に設定した kind: physical - secret: true - id: hiyama-left-roof-2154 statement: 21時54分ごろ、日山澪は自動撮影中のカメラを屋上に残して建物内へ戻った kind: truth - secret: true - id: muroi-saw-hiyama-2157 statement: 21時57分ごろ、室井邦彦は資料室へ向かう廊下で日山澪とすれ違った kind: observation - id: hiyama-killed-kanzaki-2200 statement: 22時00分ごろ、日山澪は資料室で神崎遼を襲い死亡させた kind: truth - secret: true - id: hiyama-returned-roof-2206 statement: 22時06分ごろ、日山澪は屋上へ戻った kind: truth - secret: true - id: camera-kept-shooting statement: 21時50分から22時11分まで、屋上のカメラは撮影者が触れなくても三分間隔で自動撮影を続けた kind: physical - id: kurose-entered-data-room statement: 21時48分ごろ、黒瀬俊介は未公開データを複製するため解析室へ入った kind: observation - secret: true - id: muroi-broke-heater-rule statement: 室井邦彦は規則に反して私物の電気ヒーターを機械室で使っていた kind: other - secret: true - id: body-found-2215 statement: 22時15分、黒瀬俊介が資料室で神崎遼の死を発見した kind: observation timeline: - id: kanzaki-warning at: "21:40" - participants: [hiyama] - facts: [kanzaki-found-fabrication, kanzaki-would-retract] + participants: [ hiyama ] + facts: [ kanzaki-found-fabrication, kanzaki-would-retract ] description: 神崎が日山のデータ不正を指摘し、翌朝の論文撤回と報告を告げる。 + location: 観測所内 - id: kurose-data-room at: "21:48" - participants: [kurose] - facts: [kurose-entered-data-room, kurose-copied-data] + participants: [ kurose ] + facts: [ kurose-entered-data-room, kurose-copied-data ] description: 黒瀬が解析室へ入り、未公開データを無断で複製する。 + location: 解析室 - id: interval-start at: "21:50" - participants: [hiyama] - facts: [camera-interval-started, camera-kept-shooting] + participants: [ hiyama ] + facts: [ camera-interval-started, camera-kept-shooting ] + record: 撮影設定 description: 日山が屋上のカメラを三分間隔の自動撮影に設定する。 + location: 屋上 - id: hiyama-leaves-roof at: "21:54" - participants: [hiyama] - facts: [hiyama-left-roof-2154] + participants: [ hiyama ] + facts: [ hiyama-left-roof-2154 ] description: 日山がカメラを残したまま屋上を離れ、建物内へ戻る。 + location: 屋上 - id: muroi-sighting at: "21:57" - participants: [hiyama, muroi] - facts: [muroi-saw-hiyama-2157] + participants: [ hiyama, muroi ] + facts: [ muroi-saw-hiyama-2157 ] description: 室井が資料室へ向かう廊下で日山とすれ違う。 + location: 廊下 - id: kanzaki-death at: "22:00" - participants: [hiyama] - facts: [hiyama-killed-kanzaki-2200] + participants: [ hiyama ] + facts: [ hiyama-killed-kanzaki-2200 ] description: 日山が資料室で神崎を襲い、神崎は死亡する。 + location: 資料室 - id: hiyama-returns at: "22:06" - participants: [hiyama] - facts: [hiyama-returned-roof-2206] + participants: [ hiyama ] + facts: [ hiyama-returned-roof-2206 ] description: 日山が屋上へ戻り、自動撮影中のカメラのそばへ戻る。 + location: 屋上 - id: discovery at: "22:15" - participants: [kurose, hiyama, muroi] - facts: [body-found-2215] + participants: [ kurose, hiyama, muroi ] + facts: [ body-found-2215 ] description: 黒瀬が資料室で神崎の死を発見する。 + location: 資料室 characters: - id: hiyama name: 日山澪 - role: suspect publicIntroduction: "寡黙で観測に没頭する若い研究助手。" personality: 寡黙で観測に没頭する若い研究助手。数字には強い自信を持つが、研究成果を失うことへの恐怖が強い。神崎を師として尊敬していた分、見放されたと思うと感情が揺れる。 goals: - 観測データの不正補正を隠したい - 連続写真を、自分がずっと屋上にいた証拠として通したい - knowledge: [hiyama-is-assistant, camera-interval-started, camera-kept-shooting, body-found-2215] + knowledge: + [ + hiyama-is-assistant, + camera-interval-started, + camera-kept-shooting, + body-found-2215 + ] secrets: - fact: hiyama-fabricated-data disclosure: pressured @@ -155,7 +166,6 @@ characters: strategy: maintain-until-contradicted memories: - id: retraction-threat - about: kanzaki-would-retract detail: 神崎から「朝一番で撤回する。君の名前も含めて報告する」と言われたとき、何年分もの努力が消える感覚がした。 relationships: - character: kurose @@ -163,13 +173,12 @@ characters: attitude: 成果だけを持っていこうとする人だと警戒している - id: kurose name: 黒瀬俊介 - role: suspect publicIntroduction: "自信家で競争心の強い研究者。" personality: 自信家で競争心の強い研究者。神崎とは研究方針でしばしば衝突する。未公開データを無断で持ち出そうとしたため、事件と無関係でも解析室への出入りを隠したい。 goals: - 未公開データを無断コピーしたことを隠したい - 神崎との研究上の対立が殺意と誤解されるのを避けたい - knowledge: [kurose-is-collaborator, camera-kept-shooting, body-found-2215] + knowledge: [ kurose-is-collaborator, camera-kept-shooting, body-found-2215 ] secrets: - fact: kurose-copied-data disclosure: never @@ -182,7 +191,6 @@ characters: strategy: maintain-until-contradicted memories: - id: copied-drive - about: kurose-copied-data detail: 神崎が帰ったら消されるかもしれないと思い、未公開データを保存した小型ドライブをポケットに入れた感触を覚えている。 relationships: - character: hiyama @@ -190,13 +198,12 @@ characters: attitude: 優秀だが神崎に依存しすぎていると思っている - id: muroi name: 室井邦彦 - role: witness publicIntroduction: "無口で実務的な施設管理員。" personality: 無口で実務的な施設管理員。研究内容には疎いが、誰がどの廊下を通ったかはよく覚えている。規則違反の私物ヒーターを見つけられるのを恐れている。 goals: - 私物ヒーターの使用を隠したい - 廊下で見た人物については正確に話したい - knowledge: [muroi-is-caretaker, muroi-saw-hiyama-2157, body-found-2215] + knowledge: [ muroi-is-caretaker, muroi-saw-hiyama-2157, body-found-2215 ] secrets: - fact: muroi-broke-heater-rule disclosure: pressured @@ -207,7 +214,6 @@ characters: strategy: maintain-until-contradicted memories: - id: corridor-hiyama - about: muroi-saw-hiyama-2157 detail: 21時57分ごろ、屋上にいるはずの日山が資料室側から歩いてきたので、少し不思議に思った。 relationships: [] revelations: @@ -224,8 +230,8 @@ revelations: revealCondition: 日山に連続写真の撮影方法を具体的に尋ね、シャッターが自動だったことを確認した。 requires: revelations: [] - evidences: [camera-metadata] - relatedFacts: [camera-interval-started, camera-kept-shooting, hiyama-left-roof-2154] + evidences: [ camera-metadata ] + relatedFacts: [ camera-interval-started, camera-kept-shooting, hiyama-left-roof-2154 ] - id: retraction-motive title: 翌朝に撤回される論文 text: 神崎は日山のデータ不正を発見し、翌朝に論文を撤回して大学へ報告すると本人へ告げていた。 @@ -238,86 +244,74 @@ revelations: id: hiyama revealCondition: 日山に神崎から指摘された観測データと翌朝の予定を追及し、論文撤回への恐れが明確になった。 requires: - revelations: [photos-were-automatic] - evidences: [correction-notes] - relatedFacts: [hiyama-fabricated-data, kanzaki-found-fabrication, kanzaki-would-retract] + revelations: [ photos-were-automatic ] + evidences: [ correction-notes ] + relatedFacts: [ hiyama-fabricated-data, kanzaki-found-fabrication, kanzaki-would-retract ] evidences: - id: camera-metadata label: カメラのインターバル撮影設定 description: 21時50分から三分ごとの自動撮影が設定され、撮影者がシャッターに触れた記録はない。 reveal: - mode: conversation condition: 日山に撮影の操作方法を尋ねるか、黒瀬に写真が自動撮影だった可能性を確認したら開示する。 sources: - type: character id: hiyama - type: character id: kurose - supports: [camera-interval-started, camera-kept-shooting] - contradicts: ["lie:hiyama-roof-alibi"] + supports: [ camera-interval-started, camera-kept-shooting ] + contradicts: [ "lie:hiyama-roof-alibi" ] - id: corridor-sighting label: 二十一時五十七分の廊下の目撃 description: 室井は資料室へ向かう廊下で日山とすれ違っている。 reveal: - mode: conversation condition: 室井に21時50分から22時ごろの巡回中に誰と会ったか尋ねたら開示する。 sources: - type: character id: muroi - supports: [muroi-saw-hiyama-2157] - contradicts: ["lie:hiyama-roof-alibi"] + supports: [ muroi-saw-hiyama-2157 ] + contradicts: [ "lie:hiyama-roof-alibi" ] - id: correction-notes label: 神崎の観測データ検証メモ description: 日山の補正値だけが元データと合わず、翌朝の論文撤回を示す神崎の書き込みが残っている。 reveal: - mode: conversation - condition: 日山か黒瀬に神崎が直前まで調べていたデータについて尋ね、不正補正の可能性を追及したら開示する。 + condition: 日山か黒瀬に神崎が直前まで調べていたデータについて尋ね、不正補正の可能性を追及したら開示する。または遺体・現場を調べ、「神崎の観測データ検証メモ」に関わる資料を確認したら開示する。 sources: - type: character id: hiyama - type: character id: kurose - supports: [hiyama-fabricated-data, kanzaki-found-fabrication, kanzaki-would-retract] + - type: victim + id: victim + supports: [ hiyama-fabricated-data, kanzaki-found-fabrication, kanzaki-would-retract ] contradicts: [] - id: data-room-access label: 解析室の入室記録 description: 21時48分に黒瀬のカードで解析室へ入室した記録があり、未公開データの無断コピーも確認できる。 reveal: - mode: conversation condition: 黒瀬に21時台の居場所を細かく尋ね、解析室へ入っていないという説明を検証したら開示する。 sources: - type: character id: kurose - supports: [kurose-entered-data-room, kurose-copied-data] - contradicts: ["lie:kurose-stayed-room"] + supports: [ kurose-entered-data-room, kurose-copied-data ] + contradicts: [ "lie:kurose-stayed-room" ] - id: heater-record label: 機械室の私物ヒーター description: 室井の私物ヒーターが見つかるが、資料室での事件とは結びつかない。 reveal: - mode: conversation condition: 室井に機械室で規則違反の機器を使っていなかったか尋ね、強く否定したら開示する。 sources: - type: character id: muroi - supports: [muroi-broke-heater-rule] - contradicts: ["lie:muroi-no-private-heater"] + supports: [ muroi-broke-heater-rule ] + contradicts: [ "lie:muroi-no-private-heater" ] solution: culprit: hiyama summary: 犯人は日山澪。観測データの不正補正を神崎に見抜かれ、翌朝に論文を撤回して大学へ報告すると告げられた。日山は21時50分に屋上のカメラを三分間隔の自動撮影へ設定し、連続写真をアリバイに使う準備をした。21時54分ごろ屋上を離れ、21時57分には室井が資料室へ向かう廊下で日山を目撃している。22時ごろ資料室で神崎を襲った後、22時06分ごろ屋上へ戻った。その間もカメラは自動で写真を撮り続けていたため、一見すると日山がずっと撮影していたように見えた。黒瀬のデータ持ち出しと室井の規則違反は独立した隠し事である。 method: カメラの自動連続撮影を在席証明に見せかけ、屋上を離れて資料室で神崎を襲った後に屋上へ戻った motive: 観測データの不正が発覚し、論文撤回と大学への報告で研究者としての立場を失うことを恐れたため - requiredFacts: [hiyama-fabricated-data, kanzaki-would-retract, camera-interval-started, hiyama-left-roof-2154, muroi-saw-hiyama-2157, hiyama-killed-kanzaki-2200, camera-kept-shooting] secretKeywords: - 犯人は日山 - 日山が犯人 - 日山が神崎を襲 - 私が神崎を襲 - 連続写真でアリバイを偽装 -quality: - expectedQuestionCount: - min: 8 - max: 18 - requiredEvidence: - min: 2 - redHerrings: [kurose-copied-data, muroi-broke-heater-rule] - notes: 写真の存在と撮影者の在席を分離するのが中心。カメラ設定だけでは犯人を決めず、室井の廊下目撃を合わせて初めて日山のアリバイを崩す。 diff --git a/db/scenarios/snowbound-farm-morning-chores.yaml b/db/scenarios/snowbound-farm-morning-chores.yaml index 5c97ab9..38775db 100644 --- a/db/scenarios/snowbound-farm-morning-chores.yaml +++ b/db/scenarios/snowbound-farm-morning-chores.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: snowbound-farm-morning-chores meta: - title: 高原農園、雪籠りの朝 + title: "高原農園は朝を待つ" synopsis: "午前七時十分、雪に閉ざされた高原農園の事務室で、経営者の佐久間隆志が死亡しているのが発見されました。夜半から猛吹雪となり、農園へ通じる一本道は完全に塞がれています。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [雪, 農園, 時系列, 生活痕] victim: name: 佐久間隆志 introduction: 高原農園経営者 + foundAt: 2026-01-16T07:10:00+09:00 + foundIn: 事務室 + estimatedDeathAt: "2026-01-15T22:10:00+09:00" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 佐久間隆志は事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「出荷伝票の不足メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,30 +46,24 @@ facts: - id: audit-shortage-found statement: 佐久間隆志は事件当日の夜、出荷代金の一部が帳簿と合わないことに気づいた kind: motive - secret: true - id: fuyuki-diverted-sales statement: 冬木紬は数か月前から少額の出荷代金を私的に流用していた kind: motive - secret: true - id: sakuma-confronted-fuyuki statement: 21時50分ごろ、佐久間隆志は冬木紬に帳簿の不足について翌朝もう一度確認すると告げた kind: motive - secret: true - id: kuze-saw-fuyuki-office-side statement: 22時06分ごろ、久世圭太は事務室側の廊下から戻ってくる冬木紬を見た kind: observation - id: fuyuki-killed-sakuma statement: 22時10分ごろ、冬木紬は事務室で佐久間隆志を襲い死亡させた kind: truth - secret: true - id: fuyuki-left-victim-coat statement: 冬木紬は犯行後、佐久間隆志の防寒上着を台所脇へ移した kind: truth - secret: true - id: fuyuki-did-morning-chores statement: 05時35分ごろ、冬木紬は佐久間隆志が普段行う朝の家畜の世話を代わりに済ませた kind: truth - secret: true - id: kitchen-light-on statement: 05時48分ごろ、佐久間希は廊下から台所の灯りがついているのを見た kind: observation @@ -77,54 +79,59 @@ facts: - id: kuze-generator-secret statement: 久世圭太は許可なく予備発電機から自分の作業小屋へ電源を引いていた kind: other - secret: true - id: nozomi-hid-debt statement: 佐久間希は被害者から個人的に借りた金の返済期限を延ばしてもらっていた kind: other - secret: true - id: body-found-0710 statement: 07時10分、佐久間希が事務室で佐久間隆志の死を発見した kind: observation timeline: - id: shortage-discovered at: "2026-01-15T21:35:00+09:00" - participants: [fuyuki, nozomi] + participants: [] facts: [audit-shortage-found] description: 佐久間が夜の帳簿確認で出荷代金の不足に気づく。 + location: 事務室 - id: confrontation at: "2026-01-15T21:50:00+09:00" participants: [fuyuki] facts: [sakuma-confronted-fuyuki, fuyuki-diverted-sales] description: 佐久間が冬木に不足分を問い、翌朝に帳簿を再確認すると告げる。 + location: 事務室 - id: corridor-sighting at: "2026-01-15T22:06:00+09:00" participants: [fuyuki, kuze] facts: [kuze-saw-fuyuki-office-side] description: 久世が事務室側の廊下から戻ってくる冬木を見かける。 + location: 廊下 - id: sakuma-death at: "2026-01-15T22:10:00+09:00" participants: [fuyuki] facts: [fuyuki-killed-sakuma, fuyuki-left-victim-coat] description: 冬木が事務室で佐久間を襲い、その後に朝まで生きていたように見せる準備をする。 + location: 事務室 - id: staged-chores at: "2026-01-16T05:35:00+09:00" participants: [fuyuki] facts: [fuyuki-did-morning-chores, feed-board-magnet-moved, fuyuki-boots-wet-straw] + record: 朝飼い作業板 description: 冬木が普段は佐久間が行う朝の家畜の世話を代わりに済ませる。 + location: 農園内 - id: kitchen-light-seen at: "2026-01-16T05:48:00+09:00" participants: [nozomi] facts: [kitchen-light-on, nozomi-assumed-uncle-awake] description: 希が台所の灯りを見て、叔父がいつも通り起きていると思い込む。 + location: 農園内 - id: discovery at: "2026-01-16T07:10:00+09:00" participants: [nozomi, fuyuki, kuze] facts: [body-found-0710] description: 希が事務室で佐久間の死を発見する。 + location: 事務室 characters: - id: fuyuki name: 冬木紬 - role: suspect publicIntroduction: "実務に強く、農園の細かな段取りを誰より把握している主任。" personality: 実務に強く、農園の細かな段取りを誰より把握している主任。佐久間への恩義を語る一方、経営の数字を突かれると急に防御的になる。朝の作業は佐久間本人がしたという前提を崩されたくない。 goals: @@ -153,7 +160,6 @@ characters: strategy: maintain-until-contradicted memories: - id: morning-audit-threat - about: sakuma-confronted-fuyuki detail: 佐久間に「朝になったら出荷伝票を全部並べよう」と静かに言われた瞬間、逃げ道がなくなったと感じた。 relationships: - character: nozomi @@ -164,7 +170,6 @@ characters: attitude: 口数は少ないが廊下や設備の動きをよく見ているので警戒している - id: nozomi name: 佐久間希 - role: suspect publicIntroduction: "被害者の姪。" personality: 被害者の姪。几帳面で数字に強いが、個人的な借金を抱えていることを恥じている。朝に台所の灯りを見たため、叔父はその時点で生きていたと強く思い込んでいる。 goals: @@ -181,7 +186,6 @@ characters: strategy: maintain-until-contradicted memories: - id: kitchen-glow - about: kitchen-light-on detail: 五時台に廊下へ出たとき、台所の磨りガラス越しに灯りが見え、いつもの朝だと思って部屋へ戻った。 relationships: - character: fuyuki @@ -192,7 +196,6 @@ characters: attitude: 無愛想だが嘘をつくのが下手な人だと思っている - id: kuze name: 久世圭太 - role: witness publicIntroduction: "農園の設備担当。" personality: 設備の状態を数字で覚える寡黙な技術者。予備発電機を私用した規則違反を隠しているが、人の動線については余計な推測をせず見たまま話す。 goals: @@ -209,8 +212,9 @@ characters: strategy: maintain-until-contradicted memories: - id: late-corridor - about: kuze-saw-fuyuki-office-side detail: 22時すぎ、工具を取りに出たとき、冬木が事務室側から早足で戻ってきたのを覚えている。 + - id: death-estimate-memory + detail: 設備担当として事務室の夜間温度を把握し、発見時の確認では佐久間の死亡は前夜22時10分ごろと見積もられていたことを覚えている。 relationships: - character: fuyuki relation: 同僚 @@ -266,7 +270,6 @@ evidences: label: 朝飼い作業板 description: 完了磁石は移動しているが、佐久間が毎朝必ず書く作業時刻の記入がない。作業者を示す仕組みでもない。 reveal: - mode: conversation condition: 希か久世に朝飼いの通常手順と作業板の使い方を具体的に尋ねたら開示する。 sources: - type: character @@ -279,7 +282,6 @@ evidences: label: 冬木の長靴の雪解け水と藁 description: 六時すぎの冬木の長靴には、外へ出た直後と分かる雪解け水と飼料庫の藁が付いていた。 reveal: - mode: conversation condition: 久世に朝六時前後の冬木の様子や長靴について尋ねるか、冬木の就寝中という主張を検証したら開示する。 sources: - type: character @@ -292,7 +294,6 @@ evidences: label: 二十二時六分の廊下目撃 description: 久世は事務室側から戻ってくる冬木を22時06分ごろに見ている。 reveal: - mode: conversation condition: 久世に22時前後に廊下で誰を見たか尋ねたら開示する。 sources: - type: character @@ -303,20 +304,20 @@ evidences: label: 出荷伝票の不足メモ description: 佐久間の手元には不足額と冬木担当分の伝票番号、翌朝再確認する旨のメモが残っている。 reveal: - mode: conversation - condition: 希か冬木に前夜の帳簿確認と不足伝票について尋ね、経理上の問題を追及したら開示する。 + condition: 希か冬木に前夜の帳簿確認と不足伝票について尋ね、経理上の問題を追及したら開示する。または遺体・現場を調べ、「出荷伝票の不足メモ」に関わる資料を確認したら開示する。 sources: - type: character id: nozomi - type: character id: fuyuki + - type: victim + id: victim supports: [audit-shortage-found, fuyuki-diverted-sales, sakuma-confronted-fuyuki] contradicts: ["lie:fuyuki-no-account-problem"] - id: debt-note label: 希への貸付メモ description: 佐久間が希への個人的な貸付と返済猶予を書き留めた紙。事件の時系列とは結びつかない。 reveal: - mode: conversation condition: 希に叔父との金銭関係を詳しく尋ね、貸し借りを否定したら開示する。 sources: - type: character @@ -327,29 +328,30 @@ evidences: label: 予備発電機の私設配線 description: 久世が作業小屋へ無断で電源を引いていたことが分かるが、佐久間の死とは直接結びつかない。 reveal: - mode: conversation condition: 久世に予備発電機の使用先を詳しく尋ね、私用を否定したら開示する。 sources: - type: character id: kuze supports: [kuze-generator-secret] contradicts: ["lie:kuze-no-private-power"] + - id: death-estimate + label: 冬夜の死亡推定 + description: 事務室の夜間温度と発見時の状態を合わせると、佐久間の死亡は前夜22時10分ごろと見積もられる。明け方の朝仕事より大幅に早い。 + reveal: + condition: 遺体を調べて発見時の状態を確認するか、久世に事務室の夜間温度と発見時の確認内容を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: kuze } + supports: [fuyuki-killed-sakuma] + contradicts: [] + revealsDeathTime: true solution: culprit: fuyuki summary: 冬木紬は出荷代金の流用を佐久間に見抜かれ、翌朝に帳簿を再確認されることを恐れた。前夜22時台に事務室へ向かい佐久間を襲った後、明け方に佐久間が毎朝行っていた家畜の世話を代わりに済ませ、台所にも人が起きているような痕跡を残した。希は灯りを見ただけで佐久間本人を確認しておらず、朝飼いの作業板にも佐久間が毎朝残す時刻の記入がない。さらに六時すぎの冬木の長靴には外へ出た直後の雪解け水と飼料庫の藁が付いており、「朝まで部屋にいた」という説明と矛盾する。朝の生活痕は佐久間の生存証明ではなく、犯行時刻を遅く見せるための偽装だった。 method: 前夜に事務室で佐久間を襲った後、明け方に本人の朝の習慣を代行して生活痕を残し、死亡時刻を朝方だと思わせた motive: 出荷代金の流用が発覚し、翌朝の帳簿再確認で不正が確定することを恐れたため - requiredFacts: [fuyuki-diverted-sales, sakuma-confronted-fuyuki, kuze-saw-fuyuki-office-side, fuyuki-killed-sakuma, fuyuki-did-morning-chores, feed-board-magnet-moved, fuyuki-boots-wet-straw] secretKeywords: - 犯人は冬木 - 冬木が佐久間を襲 - 冬木が朝の世話を代行 - 朝支度で死亡時刻を偽装 -quality: - expectedQuestionCount: - min: 10 - max: 22 - requiredEvidence: - min: 3 - redHerrings: [nozomi-hid-debt, kuze-generator-secret] - notes: 朝の生活痕を生存証明とみなす思い込みを崩す事件。朝の痕跡だけでは冬木を特定できず、22時台の目撃、長靴、帳簿の動機を合わせて犯人を絞る。 diff --git a/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml b/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml index 8b0c923..c88841f 100644 --- a/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml +++ b/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml @@ -1,15 +1,33 @@ schemaVersion: 1 id: snowbound-gallery-wrong-crime-scene meta: - title: 私設画廊白環館、雪の夜 + title: "白環館の雪" synopsis: "午後十時五分、大雪で道路を断たれた私設画廊「白環館」で、館長・荻原直哉が作品保存庫内で死亡しているのが発見されました。" category: 不可能犯罪 difficulty: 5 estimatedMinutes: 18 - tags: [画廊, 雪, 密室, 現場誤認] victim: name: 荻原直哉 introduction: 私設画廊「白環館」館長 + foundAt: 22:05 + foundIn: 保存庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 荻原直哉は保存庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「修復工程と材料在庫の不一致」に関わる資料が残されている。 +places: + - id: framing-room + name: 額装作業室 + shortName: 額装室 + introduction: 作品台紙の加工と搬送準備を行う作業室 + situation: 作業台と資材棚が、閉館時のまま残されている + findings: + - id: framing-paper-traces + statement: 床に散った紙片は、その夜に荻原が確認していた作品台紙と同じ材質である。 + - id: cart-used-again + statement: 清掃後に戻された作品搬送台車には、その後もう一度動かされた車輪跡が残っている。 briefing: |- ——事件の記録を読み上げます。 @@ -44,27 +62,21 @@ facts: - id: forged-restoration-report statement: 香坂澪は高額作品の修復工程を実際より多く申告し、材料費の一部を水増ししていた kind: motive - secret: true - id: ogiwara-found-forgery statement: 荻原直哉は事件当日の夜、香坂澪の修復報告と材料在庫が合わないことに気づいた kind: motive - secret: true - id: ogiwara-called-kosaka statement: 21時02分ごろ、荻原直哉は香坂澪を額装作業室へ呼び、修復報告について説明を求めた kind: motive - secret: true - id: kosaka-killed-ogiwara-framing statement: 21時10分ごろ、香坂澪は額装作業室で荻原直哉を襲い死亡させた kind: truth - secret: true - id: kosaka-moved-ogiwara statement: 21時18分ごろ、香坂澪は作品搬送用の台車を使って荻原直哉を保存庫へ移した kind: truth - secret: true - id: kosaka-left-vault-before-lock statement: 21時23分ごろ、香坂澪は保存庫から出て搬送廊下へ戻った kind: truth - secret: true - id: vault-closed-2130 statement: 21時30分、保存庫が夜間環境管理へ移行し、扉が閉鎖状態になった kind: physical @@ -80,55 +92,69 @@ facts: - id: asakura-secret-private-loan statement: 朝倉凪は館の所蔵作品を無断で知人の撮影に貸し出していた kind: other - secret: true - id: tada-secret-unlogged-break statement: 多田圭は警備記録に残さず休憩時間を延ばしていた kind: other - secret: true - id: body-found-2205 statement: 22時05分、朝倉凪と多田圭が保存庫内で荻原直哉の死を発見した kind: observation timeline: - id: report-confrontation at: "21:02" - participants: [kosaka] - facts: [forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka] + participants: [ kosaka ] + facts: [ forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka ] description: 荻原が香坂を額装作業室へ呼び、修復報告と材料在庫の不一致を問いただす。 + location: 額装作業室 - id: ogiwara-death at: "21:10" - participants: [kosaka] - facts: [kosaka-killed-ogiwara-framing, framing-paper-fibers] + participants: [ kosaka ] + facts: [ kosaka-killed-ogiwara-framing, framing-paper-fibers ] + record: 台紙片 description: 香坂が額装作業室で荻原を襲う。 + location: 額装作業室 - id: transfer-to-vault at: "21:18" - participants: [kosaka] - facts: [kosaka-moved-ogiwara, cart-used-after-cleaning] + participants: [ kosaka ] + facts: [ kosaka-moved-ogiwara, cart-used-after-cleaning ] + record: 台車の使用跡 description: 香坂が作品搬送台車を使い、荻原を保存庫へ移す。 + location: 保存庫 - id: kosaka-exits-vault at: "21:23" - participants: [kosaka] - facts: [kosaka-left-vault-before-lock, kosaka-apron-dust] + participants: [ kosaka ] + facts: [ kosaka-left-vault-before-lock, kosaka-apron-dust ] + record: 台紙粉 description: 香坂が保存庫を離れ、夜間環境管理が始まる前に搬送廊下へ戻る。 + location: 搬送廊下 - id: vault-seals at: "21:30" - participants: [tada] - facts: [vault-night-mode, framing-room-open-before-2130, vault-closed-2130] + participants: [ tada ] + facts: [ vault-night-mode, framing-room-open-before-2130, vault-closed-2130 ] + record: 環境管理記録 description: 保存庫が夜間環境管理へ移行し、発見時の密室状態が成立する。 + location: 保存庫 - id: discovery at: "22:05" - participants: [asakura, tada, kosaka] - facts: [body-found-2205] + participants: [ asakura, tada, kosaka ] + facts: [ body-found-2205 ] description: 朝倉と多田が保存庫内で荻原の死を発見する。 + location: 保存庫 characters: - id: kosaka name: 香坂澪 - role: suspect publicIntroduction: "手先が器用で作品を丁寧に扱う修復士。" personality: 手先が器用で作品を丁寧に扱う修復士。保存庫が発見時に閉鎖されていたことを強調し、「中で事件が起きたなら自分には不可能」と話す。修復費の細部を追及されると苛立つ。 goals: - 修復費の水増しを隠したい - 事件現場が保存庫だったと思わせたい - knowledge: [kosaka-restorer, vault-night-mode, framing-room-open-before-2130, vault-closed-2130, body-found-2205] + knowledge: + [ + kosaka-restorer, + vault-night-mode, + framing-room-open-before-2130, + vault-closed-2130, + body-found-2205 + ] secrets: - fact: forged-restoration-report disclosure: pressured @@ -153,7 +179,6 @@ characters: strategy: maintain-until-contradicted memories: - id: cart-route - about: kosaka-moved-ogiwara detail: 額装作業室と保存庫は同じ搬送廊下にあり、夜間閉鎖前なら作品台車で行き来できることを日常業務で知っている。 relationships: - character: asakura @@ -164,13 +189,18 @@ characters: attitude: 保存庫が閉じた時刻を正確に知る相手 - id: asakura name: 朝倉凪 - role: witness publicIntroduction: "作品の所在を細かく管理する学芸員。" personality: 作品の所在を細かく管理する学芸員。保存庫で発見したため当初はそこが事件現場だと思い込んだ。所蔵作品の無断貸し出しを隠している。 goals: - 所蔵作品を無断で貸したことを隠したい - 額装作業室に残った不自然な紙片を説明したい - knowledge: [asakura-curator, framing-paper-fibers, cart-used-after-cleaning, body-found-2205] + knowledge: + [ + asakura-curator, + framing-paper-fibers, + cart-used-after-cleaning, + body-found-2205 + ] secrets: - fact: asakura-secret-private-loan disclosure: pressured @@ -181,7 +211,6 @@ characters: strategy: maintain-until-contradicted memories: - id: paper-on-floor - about: framing-paper-fibers detail: 閉館前に掃除したはずの額装作業室へ戻ると、荻原が確認していた古い台紙の細かな紙片がまた床に落ちていた。 relationships: - character: kosaka @@ -192,13 +221,19 @@ characters: attitude: 夜間閉鎖時刻については信用している - id: tada name: 多田圭 - role: suspect publicIntroduction: "画廊の警備担当。" personality: 規則を重んじる警備担当だが、自分の休憩延長だけは記録から外している。保存庫の閉鎖時刻は機械記録と照合して話せる。 goals: - 無断で休憩を延ばしたことを隠したい - 保存庫がいつ閉じたのかを正確に伝えたい - knowledge: [tada-security, vault-night-mode, framing-room-open-before-2130, vault-closed-2130, body-found-2205] + knowledge: + [ + tada-security, + vault-night-mode, + framing-room-open-before-2130, + vault-closed-2130, + body-found-2205 + ] secrets: - fact: tada-secret-unlogged-break disclosure: pressured @@ -209,7 +244,6 @@ characters: strategy: maintain-until-contradicted memories: - id: night-mode-time - about: vault-closed-2130 detail: 九時半の環境管理移行は毎晩確認している。逆に言えば、それ以前は搬送作業のため扉を使える。 relationships: - character: kosaka @@ -232,14 +266,20 @@ revelations: revealCondition: 朝倉に額装作業室の紙片と搬送台車の使用跡をまとめて確認し、保存庫以外の現場を検討したら開示する。 requires: revelations: [] - evidences: [framing-room-traces, cart-trace] + evidences: [ framing-room-traces, cart-trace ] - type: character id: tada revealCondition: 多田に保存庫が閉鎖された正確な時刻と、それ以前の搬送経路について尋ねたら開示する。 requires: revelations: [] - evidences: [night-mode-log] - relatedFacts: [framing-room-open-before-2130, kosaka-killed-ogiwara-framing, kosaka-moved-ogiwara, vault-closed-2130] + evidences: [ night-mode-log ] + relatedFacts: + [ + framing-room-open-before-2130, + kosaka-killed-ogiwara-framing, + kosaka-moved-ogiwara, + vault-closed-2130 + ] - id: restoration-motive title: 修復報告の水増し text: 荻原は香坂の修復報告と材料在庫が合わないことを発見し、事件直前に説明を求めていた。 @@ -252,117 +292,104 @@ revelations: id: kosaka revealCondition: 香坂に材料費と修復工程の不一致を具体的に示し、荻原から追及されたことを認めさせたら開示する。 requires: - revelations: [discovery-place-not-crime-scene] - evidences: [restoration-report] + revelations: [ discovery-place-not-crime-scene ] + evidences: [ restoration-report ] - type: character id: asakura revealCondition: 朝倉に荻原が事件直前まで確認していた修復書類について尋ねたら開示する。 requires: - revelations: [discovery-place-not-crime-scene] - evidences: [restoration-report] - relatedFacts: [forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka] + revelations: [ discovery-place-not-crime-scene ] + evidences: [ restoration-report ] + relatedFacts: [ forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka ] evidences: - id: night-mode-log label: 保存庫の夜間環境管理記録 description: 保存庫が外から通常操作できなくなったのは21時30分で、それ以前には搬送作業が可能だった。 reveal: - mode: conversation condition: 多田に保存庫の閉鎖時刻と閉鎖前の運用を尋ねたら開示する。 sources: - type: character id: tada - supports: [vault-night-mode, framing-room-open-before-2130, vault-closed-2130] - contradicts: ["lie:kosaka-vault-crime"] + supports: [ vault-night-mode, framing-room-open-before-2130, vault-closed-2130 ] + contradicts: [ "lie:kosaka-vault-crime" ] - id: framing-room-traces label: 額装作業室の台紙片 description: 清掃後の額装作業室に、荻原が確認していた作品台紙と同じ紙片が散っている。 reveal: - mode: conversation - condition: 朝倉に事件後の額装作業室で気づいた変化を尋ねたら開示する。 + condition: 朝倉に事件後の額装作業室で気づいた変化を尋ねたら開示する。または額装作業室を調べ、床の台紙片と作業台周辺の痕跡を確認したら開示する。 sources: - type: character id: asakura - type: character id: kosaka - supports: [framing-paper-fibers, kosaka-killed-ogiwara-framing] - contradicts: ["lie:kosaka-vault-crime"] + - { type: location, id: framing-room } + supports: [ framing-paper-fibers, kosaka-killed-ogiwara-framing ] + contradicts: [ "lie:kosaka-vault-crime" ] - id: cart-trace label: 作品搬送台車の再使用跡 description: 清掃後に所定位置へ戻した台車が21時台に再使用され、額装作業室と保存庫の間を動いた形跡がある。 reveal: - mode: conversation condition: 朝倉か香坂に作品搬送台車を事件当夜に使った者がいないか尋ねたら開示する。 sources: - type: character id: asakura - type: character id: kosaka - supports: [cart-used-after-cleaning, kosaka-moved-ogiwara] - contradicts: ["lie:kosaka-vault-crime"] + supports: [ cart-used-after-cleaning, kosaka-moved-ogiwara ] + contradicts: [ "lie:kosaka-vault-crime" ] - id: apron-dust label: 香坂の作業着の台紙粉 description: 香坂の作業着には額装作業室で扱う古い台紙の粉が多く付いている。 reveal: - mode: conversation condition: 香坂に21時台の作業場所を尋ねるか、朝倉に額装作業室特有の粉について確認したら開示する。 sources: - type: character id: kosaka - type: character id: asakura - supports: [kosaka-apron-dust] - contradicts: ["lie:kosaka-vault-crime"] + supports: [ kosaka-apron-dust ] + contradicts: [ "lie:kosaka-vault-crime" ] - id: restoration-report label: 修復工程と材料在庫の不一致 description: 香坂の報告上は使用したことになっている材料が在庫から減っておらず、荻原が確認印を付けている。 reveal: - mode: conversation - condition: 香坂に事件前に荻原から指摘された修復報告を尋ねるか、朝倉に材料在庫の不一致を確認したら開示する。 + condition: 香坂に事件前に荻原から指摘された修復報告を尋ねるか、朝倉に材料在庫の不一致を確認したら開示する。または遺体・現場を調べ、「修復工程と材料在庫の不一致」に関わる資料を確認したら開示する。 sources: - type: character id: kosaka - type: character id: asakura - supports: [forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka] - contradicts: ["lie:kosaka-report-clean"] + - type: victim + id: victim + supports: [ forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka ] + contradicts: [ "lie:kosaka-report-clean" ] - id: private-loan label: 無断貸し出し記録 description: 朝倉が所蔵作品を知人の撮影へ無断で貸していた記録。主事件とは独立している。 reveal: - mode: conversation condition: 朝倉に最近の館外貸し出しについて尋ね、無断貸し出しを否定したら開示する。 sources: - type: character id: asakura - supports: [asakura-secret-private-loan] - contradicts: ["lie:asakura-no-private-loan"] + supports: [ asakura-secret-private-loan ] + contradicts: [ "lie:asakura-no-private-loan" ] - id: extended-break label: 警備記録にない休憩 description: 多田が警備記録に残さず休憩を延ばしていたことが分かるが、保存庫閉鎖の記録とは別問題である。 reveal: - mode: conversation condition: 多田に21時台の巡回と休憩を細かく尋ね、記録外の休憩を否定したら開示する。 sources: - type: character id: tada - supports: [tada-secret-unlogged-break] - contradicts: ["lie:tada-no-extra-break"] + supports: [ tada-secret-unlogged-break ] + contradicts: [ "lie:tada-no-extra-break" ] solution: culprit: kosaka summary: 犯人は香坂澪。修復報告の水増しを荻原に見抜かれ、額装作業室へ呼び出された香坂はそこで荻原を襲った。その後、夜間環境管理が始まる前に作品搬送台車で荻原を保存庫へ移し、自分は21時23分ごろに保存庫を出た。21時30分になると保存庫が自動的に閉鎖状態となり、発見時には「中で事件が起きたのに誰も出られない」ように見えた。しかし台紙片、台車の再使用跡、香坂の作業着の粉は額装作業室を実際の事件現場として示している。密室を解く鍵は扉の開け方ではなく、事件現場の前提を疑うことだった。 method: 額装作業室で荻原を襲い、閉鎖前に保存庫へ移したうえで、夜間環境管理によって後から成立した密室を事件現場だと思わせた motive: 修復工程と材料費の水増しが発覚し、館長から責任追及されることを恐れたため - requiredFacts: [forged-restoration-report, ogiwara-called-kosaka, kosaka-killed-ogiwara-framing, kosaka-moved-ogiwara, kosaka-left-vault-before-lock, vault-closed-2130, framing-paper-fibers, cart-used-after-cleaning] secretKeywords: - 犯人は香坂 - 香坂が荻原を襲 - 香坂が保存庫へ移 - 保存庫は事件現場ではない -quality: - expectedQuestionCount: - min: 11 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [asakura-secret-private-loan, tada-secret-unlogged-break] - notes: 密室の開閉方法を探すのではなく、発見場所が事件現場だという前提を外すタイプ。既存の自動施錠ものとの差別化として、主眼を物の搬送痕と現場誤認に置く。 diff --git a/db/scenarios/snowbound-studio-roomtone-loop.yaml b/db/scenarios/snowbound-studio-roomtone-loop.yaml index 271ab52..cf2bb46 100644 --- a/db/scenarios/snowbound-studio-roomtone-loop.yaml +++ b/db/scenarios/snowbound-studio-roomtone-loop.yaml @@ -1,15 +1,33 @@ schemaVersion: 1 id: snowbound-studio-roomtone-loop meta: - title: 録音所ノース・レイク、吹雪の夜 + title: "ノース・レイクに冬が来た" synopsis: "午後十一時十八分、山中の録音所「ノース・レイク」の編集室で、音楽監督の冬木圭介が死亡しているのが見つかりました。午後十時半から吹雪で道路が閉鎖され、館内には冬木を含め五人しかいませんでした。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [録音スタジオ, 吹雪, 音声, ループ] victim: name: 冬木圭介 introduction: 録音所「ノース・レイク」音楽監督 + foundAt: 23:18 + foundIn: 編集室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 冬木圭介は編集室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「未公開音源の複製履歴」に関わる資料が残されている。 +places: + - id: patch-bay + name: 第2ブース監視席 + shortName: 監視席 + introduction: 録音入力を切り替えるパッチ盤と収録端末の席 + situation: パッチ盤と収録端末の電源が残っている + findings: + - id: recorder-loop-route + statement: 第2ブースの録音入力は生マイクではなく、四十七秒の音声素材を繰り返す経路へ切り替えられている。 + - id: waveform-identical + statement: 収録波形は空調音や小さな物音まで四十七秒ごとに同じ形を繰り返している。 briefing: |- ——事件の記録を読み上げます。 @@ -41,56 +59,45 @@ facts: - id: manabe-copied-masters statement: 真鍋伊織は未公開の音源データを許可なく複製し、外部へ渡していた kind: motive - secret: true - id: fuyuki-found-master-leak statement: 冬木圭介は事件当日、真鍋伊織による未公開音源の持ち出しを突き止めた kind: motive - secret: true - id: fuyuki-planned-report statement: 冬木圭介は翌朝、真鍋伊織の音源持ち出しを制作会社へ報告し、機材アクセス権を停止する予定だった kind: motive - secret: true - id: manabe-made-roomtone-loop statement: 22時46分、真鍋伊織は第2ブースで録った四十七秒の室内音を連続再生できるループ素材にした kind: physical - secret: true - id: loop-routed-to-recorder statement: 22時50分から23時10分まで、第2ブースの録音機には生マイクではなく四十七秒のループ素材が入力されていた kind: physical - secret: true - id: recording-repeats-identically statement: 22時50分から23時10分の収録ファイルでは、空調音や小さな物音を含む波形が四十七秒ごとに完全一致している kind: physical - id: manabe-left-booth statement: 22時53分ごろ、真鍋伊織は第2ブースの監視席を離れた kind: truth - secret: true - id: makimura-saw-manabe statement: 22時58分ごろ、牧村葉月は編集室へ続く機材廊下で真鍋伊織を見た kind: observation - id: manabe-killed-fuyuki statement: 23時02分ごろ、真鍋伊織は編集室で冬木圭介を襲い死亡させた kind: truth - secret: true - id: manabe-returned-booth statement: 23時08分ごろ、真鍋伊織は第2ブースの監視席へ戻った kind: truth - secret: true - id: shirase-heard-loop statement: 22時56分ごろ、白瀬環は第2ブース前の廊下で録音中のような室内音を聞いた kind: observation - id: takano-hid-contract-change statement: 鷹野徹は出演条件の変更を正式承認前に処理し、制作費を別項目へ付け替えていた kind: other - secret: true - id: shirase-broke-embargo statement: 白瀬環は公開前の新曲情報を親しい知人へ話していた kind: other - secret: true - id: makimura-damaged-microphone statement: 牧村葉月は高価なマイクを落として傷つけたことを報告せず、別のケースへ戻していた kind: other - secret: true - id: body-found-2318 statement: 23時18分、鷹野徹が編集室で冬木圭介の死を発見した kind: observation @@ -100,47 +107,56 @@ timeline: at: "22:46" participants: [manabe] facts: [manabe-made-roomtone-loop] + record: ループ素材 description: 真鍋が第2ブースの四十七秒の室内音をループ素材として作成する。 + location: 第2ブース - id: loop-recording-starts at: "22:50" participants: [manabe] facts: [loop-routed-to-recorder, recording-repeats-identically] + record: 収録ファイル description: 第2ブースの録音機へループ素材が入力され、連続収録が始まる。 + location: 第2ブース - id: manabe-leaves at: "22:53" participants: [manabe] facts: [manabe-left-booth] description: 真鍋が録音を動かしたまま監視席を離れる。 + location: 監視席 - id: shirase-hearing at: "22:56" participants: [shirase] facts: [shirase-heard-loop] description: 白瀬が廊下から第2ブースの録音中らしい音を聞く。 + location: 第2ブース - id: makimura-sighting at: "22:58" participants: [manabe, makimura] facts: [makimura-saw-manabe] description: 牧村が編集室へ続く機材廊下で真鍋を目撃する。 + location: 機材廊下 - id: fuyuki-death at: "23:02" participants: [manabe] facts: [manabe-killed-fuyuki] description: 真鍋が編集室で冬木を襲い、冬木は死亡する。 + location: 編集室 - id: manabe-return at: "23:08" participants: [manabe] facts: [manabe-returned-booth] description: 真鍋が第2ブースの監視席へ戻る。 + location: 監視席 - id: discovery at: "23:18" participants: [takano, manabe, shirase, makimura] facts: [body-found-2318] description: 鷹野が編集室で冬木の死を発見する。 + location: 編集室 characters: - id: manabe name: 真鍋伊織 - role: suspect publicIntroduction: "録音所の録音技師。" personality: 音の差異に異常なほど敏感で、自分の技術に強い自負を持つ録音技師。説明は専門的だが、連続した収録ファイルを在席の客観証拠として押し出し、入力経路の話になると曖昧になる。 goals: @@ -175,7 +191,6 @@ characters: strategy: maintain-until-contradicted memories: - id: access-stop-threat - about: fuyuki-planned-report detail: 冬木に「朝になったら会社へ報告して、君のアクセス権を止める」と言われ、過去の持ち出しまで全部調べられると思った。 relationships: - character: makimura @@ -186,7 +201,6 @@ characters: attitude: 金の処理ばかり気にして音を理解しない人だと思っている - id: shirase name: 白瀬環 - role: witness publicIntroduction: "感覚的に物事を捉える歌手。" personality: 感覚的に物事を捉える歌手。公開前情報を知人へ話したことを隠したいが、廊下で聞いた音については率直に答える。音が聞こえたため最初は真鍋が中にいると思い込んでいる。 goals: @@ -203,7 +217,6 @@ characters: strategy: maintain-until-contradicted memories: - id: booth-sound-memory - about: shirase-heard-loop detail: 22時56分ごろ、ブース前を通ったとき空調の低い音と椅子が小さく鳴るような音が続いていて、収録中だと思って静かに通り過ぎた。 relationships: - character: manabe @@ -211,7 +224,6 @@ characters: attitude: 技術は信頼しているが、音源データの扱いには神経質すぎると感じている - id: takano name: 鷹野徹 - role: suspect publicIntroduction: "録音所の制作進行。" personality: 現実的で数字に強い制作進行。契約処理の付け替えが露見するのを恐れているため冬木との事務的な衝突を小さく見せるが、音響機器の細部には疎い。 goals: @@ -228,7 +240,6 @@ characters: strategy: maintain-until-contradicted memories: - id: fuyuki-angry-at-manabe - about: fuyuki-found-master-leak detail: 事件前、冬木が真鍋に「これは技術の問題じゃなく信用の問題だ」と低い声で言っているのを聞いた。 relationships: - character: shirase @@ -236,7 +247,6 @@ characters: attitude: 情報管理が甘いところを心配している - id: makimura name: 牧村葉月 - role: witness publicIntroduction: "配線と機材配置を記憶するのが得意な機材担当。" personality: 配線と機材配置を記憶するのが得意な機材担当。高価なマイクを傷つけたことを隠したいが、パッチ盤の入力経路と廊下で見た真鍋については正確に話せる。 goals: @@ -253,7 +263,6 @@ characters: strategy: maintain-until-contradicted memories: - id: patch-bay-route - about: loop-routed-to-recorder detail: 事件後に第2ブースのパッチ盤を見ると、生マイクではなく編集機側の再生出力が録音機へ戻されていたのが気になった。 relationships: [] @@ -315,18 +324,17 @@ evidences: label: 四十七秒ごとに一致する収録波形 description: 空調音や小さな物音まで含めた波形が四十七秒周期で完全一致し、同じ室内音が繰り返し再生されていたと分かる。 reveal: - mode: conversation - condition: 真鍋か牧村に収録ファイルが本当に生マイク入力だったか、波形の反復と入力経路を含めて尋ねたら開示する。 + condition: 真鍋か牧村に収録ファイルが本当に生マイク入力だったか、波形の反復と入力経路を含めて尋ねたら開示する。または第2ブースの監視席を調べ、録音入力と波形の反復を確認したら開示する。 sources: - { type: character, id: manabe } - { type: character, id: makimura } + - { type: location, id: patch-bay } supports: [manabe-made-roomtone-loop, loop-routed-to-recorder, recording-repeats-identically] contradicts: ["lie:manabe-booth-alibi", "lie:manabe-live-input"] - id: equipment-corridor-sighting label: 二十二時五十八分の機材廊下目撃 description: 牧村は22時58分ごろ、編集室へ続く機材廊下で真鍋を見ている。 reveal: - mode: conversation condition: 牧村に22時50分から23時ごろ機材廊下で誰を見たか尋ねたら開示する。 sources: - { type: character, id: makimura } @@ -336,18 +344,17 @@ evidences: label: 未公開音源の複製履歴 description: 真鍋の作業端末から未公開マスターが外部媒体へ複製され、冬木が翌朝のアクセス停止を記したメモを残している。 reveal: - mode: conversation - condition: 真鍋か鷹野に冬木が事件前に調べていた音源データの持ち出しについて尋ね、複製履歴を追及したら開示する。 + condition: 真鍋か鷹野に冬木が事件前に調べていた音源データの持ち出しについて尋ね、複製履歴を追及したら開示する。または遺体・現場を調べ、「未公開音源の複製履歴」に関わる資料を確認したら開示する。 sources: - { type: character, id: manabe } - { type: character, id: takano } + - { type: victim, id: victim } supports: [manabe-copied-masters, fuyuki-found-master-leak, fuyuki-planned-report] contradicts: [] - id: shirase-message label: 白瀬の新曲情報メッセージ description: 白瀬が公開前の新曲情報を知人へ送っていたことが分かるが、編集室の事件とは独立している。 reveal: - mode: conversation condition: 白瀬に公開前情報を外部へ話していないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: shirase } @@ -357,7 +364,6 @@ evidences: label: 鷹野の制作費付け替え表 description: 鷹野が承認前に制作費項目を付け替えたことが分かるが、冬木の死亡とは別件である。 reveal: - mode: conversation condition: 鷹野に出演条件変更と制作費処理について尋ね、付け替えをしていないという説明を検証したら開示する。 sources: - { type: character, id: takano } @@ -367,7 +373,6 @@ evidences: label: 傷のある収録用マイク description: 牧村が落としたマイクと隠したケースが見つかるが、事件とは無関係の機材事故だった。 reveal: - mode: conversation condition: 牧村に今夜機材を傷つけていないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: makimura } @@ -379,18 +384,9 @@ solution: summary: 犯人は真鍋伊織。未公開音源を外部へ渡していたことを冬木に見抜かれ、翌朝に制作会社へ報告され機材アクセス権を止められる予定だった。真鍋は22時46分に四十七秒の室内音をループ素材にし、22時50分から第2ブースの録音機へ繰り返し入力した。22時53分ごろ監視席を離れ、22時58分には牧村が編集室へ続く機材廊下で真鍋を目撃している。23時02分ごろ冬木を襲い、23時08分ごろ監視席へ戻った。二十分の収録は途切れていなかったが、空調音や小さな物音まで四十七秒ごとに完全一致しており、生の収録ではなかった。 method: 四十七秒の室内音を録音機へループ入力し、連続収録を在席証明に見せかけて監視席を離れ、編集室で冬木を襲った後に戻った motive: 未公開音源の無断持ち出しが発覚し、翌朝の報告で機材アクセス権と仕事上の信用を失うことを恐れたため - requiredFacts: [manabe-copied-masters, fuyuki-planned-report, manabe-made-roomtone-loop, loop-routed-to-recorder, recording-repeats-identically, manabe-left-booth, makimura-saw-manabe, manabe-killed-fuyuki] secretKeywords: - 犯人は真鍋 - 真鍋が犯人 - 真鍋が冬木を襲 - 私が冬木を襲 - ループ音声でアリバイを偽装 -quality: - expectedQuestionCount: - min: 13 - max: 26 - requiredEvidence: - min: 3 - redHerrings: [shirase-broke-embargo, takano-hid-contract-change, makimura-damaged-microphone] - notes: 「音が聞こえた」「録音が続いていた」という二重の客観性を、同じ四十七秒のループが同時に説明してしまう構造。波形の反復だけでなく牧村の目撃を合わせ、真鍋の実際の移動を確定させる。 diff --git a/db/scenarios/snowbound-theater-auto-cues.yaml b/db/scenarios/snowbound-theater-auto-cues.yaml index f74a26c..16337a0 100644 --- a/db/scenarios/snowbound-theater-auto-cues.yaml +++ b/db/scenarios/snowbound-theater-auto-cues.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: snowbound-theater-auto-cues meta: - title: 小劇場白燕座、雪封じの夜 - synopsis: "午後十時三十六分、市外れの小劇場「白燕座」の演出控室で、演出家の瀬尾雅人が死亡しているのが見つかりました。午後九時半から大雪で道路が封鎖され、劇場内に残っていたのは瀬尾を含め五人だけです。" + title: "白燕座、幕間" + synopsis: "午後十時三十六分、市外れの小劇場「白燕座」の演出控室で、演出家の瀬尾雅人が死亡しているのが見つかりました。午後九時半から大雪で道路が封鎖\ + され、劇場内に残っていたのは瀬尾を含め五人だけです。" category: クローズドサークル difficulty: 4 estimatedMinutes: 15 - tags: [劇場, 大雪, 照明キュー, 自動進行] victim: name: 瀬尾雅人 introduction: 小劇場「白燕座」演出家 + foundAt: 22:36 + foundIn: 演出控室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 瀬尾雅人は演出控室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「架空スタッフを含む残業費一覧」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,55 +49,45 @@ facts: - id: kunieda-falsified-overtime statement: 国枝美冬は存在しない外部スタッフ名義を使い、残業費の一部を架空請求していた kind: motive - secret: true - id: seo-found-false-overtime statement: 瀬尾雅人は事件当日、国枝美冬の残業費請求に実在しないスタッフ名が含まれることを発見した kind: motive - secret: true - id: seo-planned-report statement: 瀬尾雅人は翌朝、国枝美冬の架空請求を劇場運営会社へ報告し、公演の進行担当から外す予定だった kind: motive - secret: true - id: cue-console-auto-mode statement: 調光卓には通し稽古用に、設定済みの時刻間隔で照明キューを自動進行させる機能がある kind: physical - id: kunieda-set-auto-cues statement: 21時57分、国枝美冬は22時00分から二十分間の照明キューを自動進行へ設定した kind: physical - secret: true - id: cues-ran-automatically statement: 22時00分から22時20分まで、照明キューは調光卓への手動入力なしで設定順に自動進行した kind: physical - id: kunieda-left-console statement: 22時03分ごろ、国枝美冬は調光卓を離れた kind: truth - secret: true - id: sasai-saw-kunieda-2210 statement: 22時10分ごろ、笹井徹は演出控室へ続く舞台袖通路で国枝美冬を見た kind: observation - id: kunieda-killed-seo statement: 22時14分ごろ、国枝美冬は演出控室で瀬尾雅人を襲い死亡させた kind: truth - secret: true - id: kunieda-returned-console statement: 22時19分ごろ、国枝美冬は調光卓へ戻った kind: truth - secret: true - id: toba-saw-light-changes statement: 22時00分から22時20分の間、鳥羽香は客席から照明が台本どおり切り替わり続けるのを見た kind: observation - id: hiiragi-hid-script-leak statement: 柊真琴は公開前の改稿台本を知人へ送っていた kind: other - secret: true - id: toba-hid-costume-damage statement: 鳥羽香は高価な衣装を傷めたことを報告せず、自分で補修して隠していた kind: other - secret: true - id: sasai-bypassed-inspection statement: 笹井徹は舞台機構の定期点検を一項目省略し、実施済みとして記録していた kind: other - secret: true - id: body-found-2236 statement: 22時36分、柊真琴が演出控室で瀬尾雅人の死を発見した kind: observation @@ -97,55 +95,72 @@ facts: timeline: - id: auto-cues-set at: "21:57" - participants: [kunieda] - facts: [cue-console-auto-mode, kunieda-set-auto-cues] + participants: [ kunieda ] + facts: [ cue-console-auto-mode, kunieda-set-auto-cues ] + record: 調光卓履歴 description: 国枝が二十分間の照明キューを自動進行へ設定する。 + location: 調光卓 - id: cues-start at: "22:00" - participants: [kunieda, toba] - facts: [cues-ran-automatically, toba-saw-light-changes] + participants: [ kunieda, toba ] + facts: [ cues-ran-automatically, toba-saw-light-changes ] + record: キュー履歴 description: 調光卓が自動進行を開始し、舞台照明が設定順に切り替わる。 + location: 調光卓 - id: kunieda-leaves at: "22:03" - participants: [kunieda] - facts: [kunieda-left-console] + participants: [ kunieda ] + facts: [ kunieda-left-console ] description: 国枝が照明キューを動かしたまま調光卓を離れる。 + location: 調光卓 - id: sasai-sighting at: "22:10" - participants: [kunieda, sasai] - facts: [sasai-saw-kunieda-2210] + participants: [ kunieda, sasai ] + facts: [ sasai-saw-kunieda-2210 ] description: 笹井が演出控室へ続く舞台袖通路で国枝を目撃する。 + location: 舞台袖 - id: seo-death at: "22:14" - participants: [kunieda] - facts: [kunieda-killed-seo] + participants: [ kunieda ] + facts: [ kunieda-killed-seo ] description: 国枝が演出控室で瀬尾を襲い、瀬尾は死亡する。 + location: 演出控室 - id: kunieda-return at: "22:19" - participants: [kunieda] - facts: [kunieda-returned-console] + participants: [ kunieda ] + facts: [ kunieda-returned-console ] description: 国枝が調光卓へ戻る。 + location: 調光卓 - id: cues-end at: "22:20" - participants: [kunieda, toba] - facts: [cues-ran-automatically] + participants: [ kunieda, toba ] + facts: [ cues-ran-automatically ] + record: キュー履歴 description: 自動進行の照明キューが最後まで終了する。 + location: 調光卓 - id: discovery at: "22:36" - participants: [hiiragi, kunieda, toba, sasai] - facts: [body-found-2236] + participants: [ hiiragi, kunieda, toba, sasai ] + facts: [ body-found-2236 ] description: 柊が演出控室で瀬尾の死を発見する。 + location: 演出控室 characters: - id: kunieda name: 国枝美冬 - role: suspect publicIntroduction: "段取りに厳しく、舞台上の出来事を秒単位で管理する舞台監督。" personality: 段取りに厳しく、舞台上の出来事を秒単位で管理する舞台監督。照明が予定どおり進んだ事実を自分の操作記録のように語り、自動進行機能について尋ねられると話をそらす。 goals: - 架空スタッフ名義の残業費請求を隠したい - 二十分続いた照明変化を、自分が調光卓に居続けた証拠として通したい - knowledge: [kunieda-stage-manager, cue-console-auto-mode, cues-ran-automatically, toba-saw-light-changes, body-found-2236] + knowledge: + [ + kunieda-stage-manager, + cue-console-auto-mode, + cues-ran-automatically, + toba-saw-light-changes, + body-found-2236 + ] secrets: - fact: kunieda-falsified-overtime disclosure: pressured @@ -172,7 +187,6 @@ characters: strategy: maintain-until-contradicted memories: - id: removal-threat - about: seo-planned-report detail: 瀬尾から「明朝、運営会社へ出す。次の公演から進行は外れてもらう」と言われ、劇場での立場が終わると思った。 relationships: - character: sasai @@ -183,13 +197,12 @@ characters: attitude: 客席から照明変化を見ていたので、自分のアリバイを補強してくれると思っている - id: hiiragi name: 柊真琴 - role: suspect publicIntroduction: "感情表現の大きい主演俳優。" personality: 感情表現の大きい主演俳優。改稿台本を知人へ送ったことを隠したいが、舞台機器の仕組みには詳しくない。瀬尾と配役を巡って口論していた。 goals: - 公開前の台本を外部へ送ったことを隠したい - 瀬尾との口論だけで疑われたくない - knowledge: [hiiragi-actor, body-found-2236] + knowledge: [ hiiragi-actor, body-found-2236 ] secrets: - fact: hiiragi-hid-script-leak disclosure: pressured @@ -200,18 +213,22 @@ characters: strategy: maintain-until-contradicted memories: - id: seo-angry-at-kunieda - about: seo-found-false-overtime detail: 事件前、瀬尾が国枝に「実在しない名前で請求するのは演出の都合じゃ済まない」と言っていたのを聞いた。 relationships: [] - id: toba name: 鳥羽香 - role: witness publicIntroduction: "細部を見る衣装担当。" personality: 細部を見る衣装担当。衣装の損傷を隠したいが、客席から照明が二十分間切り替わった事実は正確に話す。ただし誰が調光卓を操作していたかは見えていない。 goals: - 衣装を傷めて隠したことを秘密にしたい - 照明が台本どおり変わったことと、操作者を見ていないことを分けて話したい - knowledge: [toba-costume, toba-saw-light-changes, cues-ran-automatically, body-found-2236] + knowledge: + [ + toba-costume, + toba-saw-light-changes, + cues-ran-automatically, + body-found-2236 + ] secrets: - fact: toba-hid-costume-damage disclosure: pressured @@ -222,18 +239,22 @@ characters: strategy: maintain-until-contradicted memories: - id: lights-but-no-operator - about: toba-saw-light-changes detail: 客席から照明の変化は全部見えたが、調光卓は後方の壁際で人影までは見えなかった。 relationships: [] - id: sasai name: 笹井徹 - role: witness publicIntroduction: "舞台機構の挙動に詳しい技術者。" personality: 舞台機構の挙動に詳しい技術者。点検省略を隠したいが、22時10分の国枝の目撃と調光卓の自動進行機能は正確に説明できる。 goals: - 舞台機構の定期点検を一項目省略したことを隠したい - 22時10分の国枝の目撃と自動進行機能は正確に話したい - knowledge: [sasai-rigging, sasai-saw-kunieda-2210, cue-console-auto-mode, body-found-2236] + knowledge: + [ + sasai-rigging, + sasai-saw-kunieda-2210, + cue-console-auto-mode, + body-found-2236 + ] secrets: - fact: sasai-bypassed-inspection disclosure: pressured @@ -244,7 +265,6 @@ characters: strategy: maintain-until-contradicted memories: - id: wing-kunieda - about: sasai-saw-kunieda-2210 detail: 22時10分ごろ、調光卓にいるはずの国枝が演出控室側の舞台袖通路を急いで歩いていた。 relationships: [] @@ -260,14 +280,21 @@ revelations: revealCondition: 笹井に通し稽古で照明キューを自動進行できるか尋ね、手動操作が不要な機能を確認した。 requires: revelations: [] - evidences: [cue-console-history] + evidences: [ cue-console-history ] - type: character id: toba revealCondition: 鳥羽に照明変化は見たが調光卓の操作者まで見ていたか尋ね、観察の範囲を切り分けた。 requires: revelations: [] - evidences: [cue-console-history] - relatedFacts: [cue-console-auto-mode, kunieda-set-auto-cues, cues-ran-automatically, kunieda-left-console, toba-saw-light-changes] + evidences: [ cue-console-history ] + relatedFacts: + [ + cue-console-auto-mode, + kunieda-set-auto-cues, + cues-ran-automatically, + kunieda-left-console, + toba-saw-light-changes + ] - id: kunieda-wing-contradiction title: キュー中のはずの舞台袖 text: 22時10分ごろ、笹井は演出控室へ続く舞台袖通路で国枝を目撃しており、調光卓を離れなかったという説明と両立しない。 @@ -278,9 +305,9 @@ revelations: id: sasai revealCondition: 笹井に22時前後の舞台袖で会った人物を尋ね、国枝の目撃時刻を具体化した。 requires: - revelations: [cues-do-not-prove-operator] - evidences: [wing-sighting] - relatedFacts: [kunieda-left-console, sasai-saw-kunieda-2210] + revelations: [ cues-do-not-prove-operator ] + evidences: [ wing-sighting ] + relatedFacts: [ kunieda-left-console, sasai-saw-kunieda-2210 ] - id: kunieda-overtime-motive title: 翌朝に発覚する架空スタッフ text: 瀬尾は国枝の残業費請求に実在しないスタッフ名が含まれることを見抜き、翌朝に運営会社へ報告して国枝を公演進行から外す予定だった。 @@ -291,98 +318,90 @@ revelations: id: kunieda revealCondition: 国枝に瀬尾が確認していた残業費請求と翌朝の担当変更を追及し、進行担当を外される恐れを明確にした。 requires: - revelations: [kunieda-wing-contradiction] - evidences: [overtime-name-check] + revelations: [ kunieda-wing-contradiction ] + evidences: [ overtime-name-check ] - type: character id: hiiragi revealCondition: 柊に事件前の瀬尾と国枝の口論内容を尋ね、実在しないスタッフ名の請求へ話をつなげた。 requires: revelations: [] - evidences: [overtime-name-check] - relatedFacts: [kunieda-falsified-overtime, seo-found-false-overtime, seo-planned-report] + evidences: [ overtime-name-check ] + relatedFacts: [ kunieda-falsified-overtime, seo-found-false-overtime, seo-planned-report ] evidences: - id: cue-console-history label: 調光卓の自動進行履歴 description: 21時57分に二十分間の自動進行が設定され、22時00分から20分まで手動キュー入力なしで照明が切り替わっている。 reveal: - mode: conversation condition: 国枝、笹井、鳥羽のいずれかに22時台の照明キューが手動だったか、自動進行履歴を含めて尋ねたら開示する。 sources: - { type: character, id: kunieda } - { type: character, id: sasai } - { type: character, id: toba } - supports: [cue-console-auto-mode, kunieda-set-auto-cues, cues-ran-automatically, toba-saw-light-changes] - contradicts: ["lie:kunieda-console-alibi", "lie:kunieda-no-auto-cues"] + supports: + [ + cue-console-auto-mode, + kunieda-set-auto-cues, + cues-ran-automatically, + toba-saw-light-changes + ] + contradicts: [ "lie:kunieda-console-alibi", "lie:kunieda-no-auto-cues" ] - id: wing-sighting label: 二十二時十分の舞台袖目撃 description: 笹井は22時10分ごろ、演出控室へ続く舞台袖通路で国枝を見ている。 reveal: - mode: conversation condition: 笹井に22時05分から15分ごろ舞台袖で誰を見たか尋ねたら開示する。 sources: - { type: character, id: sasai } - supports: [sasai-saw-kunieda-2210] - contradicts: ["lie:kunieda-console-alibi"] + supports: [ sasai-saw-kunieda-2210 ] + contradicts: [ "lie:kunieda-console-alibi" ] - id: overtime-name-check label: 架空スタッフを含む残業費一覧 description: 国枝の請求に実在しないスタッフ名が複数含まれ、瀬尾が翌朝の運営会社報告と進行担当変更を記している。 reveal: - mode: conversation - condition: 国枝か柊に瀬尾が事件前に問題視していた残業費請求について尋ね、スタッフ名の実在性を追及したら開示する。 + condition: 国枝か柊に瀬尾が事件前に問題視していた残業費請求について尋ね、スタッフ名の実在性を追及したら開示する。または遺体・現場を調べ、「架空スタッフを含む残業費一覧」に関わる資料を確認したら開示する。 sources: - { type: character, id: kunieda } - { type: character, id: hiiragi } - supports: [kunieda-falsified-overtime, seo-found-false-overtime, seo-planned-report] + - { type: victim, id: victim } + supports: [ kunieda-falsified-overtime, seo-found-false-overtime, seo-planned-report ] contradicts: [] - id: script-leak-message label: 柊の改稿台本送信履歴 description: 柊が公開前の改稿台本を知人へ送っていたことが分かるが、演出控室の事件とは独立している。 reveal: - mode: conversation condition: 柊に改稿台本を外部へ送っていないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: hiiragi } - supports: [hiiragi-hid-script-leak] - contradicts: ["lie:hiiragi-no-script-leak"] + supports: [ hiiragi-hid-script-leak ] + contradicts: [ "lie:hiiragi-no-script-leak" ] - id: costume-repair label: 鳥羽が隠した衣装補修 description: 鳥羽が高価な衣装を傷め、自分で補修して報告しなかったことが分かるが、事件とは別件である。 reveal: - mode: conversation condition: 鳥羽に今夜衣装を傷めて隠していないか尋ね、補修跡を確認したら開示する。 sources: - { type: character, id: toba } - supports: [toba-hid-costume-damage] - contradicts: ["lie:toba-no-costume-damage"] + supports: [ toba-hid-costume-damage ] + contradicts: [ "lie:toba-no-costume-damage" ] - id: rigging-check-gap label: 笹井の点検省略記録 description: 笹井が舞台機構の点検を一項目省略していたことが分かるが、瀬尾の死亡とは無関係である。 reveal: - mode: conversation condition: 笹井に定期点検を省略していないか尋ね、実施記録を検証したら開示する。 sources: - { type: character, id: sasai } - supports: [sasai-bypassed-inspection] - contradicts: ["lie:sasai-no-skipped-check"] + supports: [ sasai-bypassed-inspection ] + contradicts: [ "lie:sasai-no-skipped-check" ] solution: culprit: kunieda summary: 犯人は国枝美冬。実在しない外部スタッフ名義で残業費を架空請求していたことを瀬尾に見抜かれ、翌朝に運営会社へ報告され公演進行から外される予定だった。国枝は21時57分に二十分間の照明キューを自動進行へ設定した。22時から舞台照明は台本どおり切り替わり続け、鳥羽も客席からそれを見ていたが、調光卓には手動入力がなかった。国枝本人は22時03分ごろ卓を離れ、22時10分には笹井が演出控室側の舞台袖で目撃している。22時14分ごろ瀬尾を襲い、22時19分ごろ調光卓へ戻った。照明の変化は舞台監督の在席ではなく、自動進行機能の動作を示していた。 method: 通し稽古用の照明キューを自動進行させ、客席から見える連続した照明変化を在席証明に見せかけて演出控室へ移動した motive: 架空スタッフ名義の残業費請求が発覚し、翌朝の報告で公演進行の担当と信用を失うことを恐れたため - requiredFacts: [kunieda-falsified-overtime, seo-planned-report, cue-console-auto-mode, kunieda-set-auto-cues, cues-ran-automatically, kunieda-left-console, sasai-saw-kunieda-2210, kunieda-killed-seo] secretKeywords: - 犯人は国枝 - 国枝が犯人 - 国枝が瀬尾を襲 - 私が瀬尾を襲 - 自動キューでアリバイを偽装 -quality: - expectedQuestionCount: - min: 11 - max: 23 - requiredEvidence: - min: 3 - redHerrings: [hiiragi-hid-script-leak, toba-hid-costume-damage, sasai-bypassed-inspection] - notes: 観客から見える照明変化を人の手動操作と誤認させる。鳥羽の『照明は見たが操作者は見ていない』という観察範囲と、笹井の舞台袖目撃を重ねてアリバイを崩す。 diff --git a/db/scenarios/storm-aquarium-feeding-lamp.yaml b/db/scenarios/storm-aquarium-feeding-lamp.yaml index 0c1866b..85fad9e 100644 --- a/db/scenarios/storm-aquarium-feeding-lamp.yaml +++ b/db/scenarios/storm-aquarium-feeding-lamp.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: storm-aquarium-feeding-lamp meta: - title: 海浜水族館、高潮の夜 - synopsis: "午前零時十二分、海浜水族館の検疫準備室で、飼育部長の江波慎吾が死亡しているのが見つかりました。高潮で正面道路は通行止めとなり、防潮シャッターも閉鎖。午後十一時半以降、館内へ出入りした者はいません。" + title: "深夜水族館の謎" + synopsis: "午前零時十二分、海浜水族館の検疫準備室で、飼育部長の江波慎吾が死亡しているのが見つかりました。高潮で正面道路は通行止めとなり、防潮シャッ\ + ターも閉鎖。午後十一時半以降、館内へ出入りした者はいません。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [水族館, 暴風, 自動給餌, アリバイ] victim: name: 江波慎吾 introduction: 海浜水族館飼育部長 + foundAt: 00:12 + foundIn: 検疫準備室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 江波慎吾は検疫準備室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「飼育記録の版差分」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +49,12 @@ facts: - id: morishita-falsified-records statement: 森下莉央は担当水槽で続いた生体の損失を少なく見せるため、飼育記録の数値を書き換えていた kind: motive - secret: true - id: enami-found-fraud statement: 江波慎吾は事件当日、森下莉央による飼育記録の改ざんを発見した kind: motive - secret: true - id: enami-planned-report statement: 江波慎吾は翌朝、森下莉央の記録改ざんを館長へ報告し、担当から外す予定だった kind: motive - secret: true - id: feeder-auto-capable statement: 深海水槽の給餌装置には設定した間隔で自動運転する機能がある kind: physical @@ -59,37 +64,30 @@ facts: - id: morishita-set-auto-feeder statement: 23時51分、森下莉央は深海水槽の給餌装置を四分間隔の自動運転に設定した kind: physical - secret: true - id: feeder-cycled statement: 23時55分、23時59分、00時03分、00時07分に給餌装置が自動で作動し、青い給餌灯が点いた kind: physical - id: morishita-left-station statement: 23時57分ごろ、森下莉央は給餌操作台を離れた kind: truth - secret: true - id: sagara-saw-morishita statement: 00時02分ごろ、相良芳江は検疫準備室へ続くバックヤード通路で森下莉央を見た kind: observation - id: morishita-killed-enami statement: 00時05分ごろ、森下莉央は検疫準備室で江波慎吾を襲い死亡させた kind: truth - secret: true - id: morishita-returned statement: 00時09分ごろ、森下莉央は深海水槽の給餌操作台へ戻った kind: truth - secret: true - id: sakakibara-hid-stock statement: 榊原直は期限管理の不備を隠すため、処分予定だった薬品在庫の記録を後から修正していた kind: other - secret: true - id: mikoshiba-private-camera statement: 御子柴徹は許可を得ずに水槽内へ私物カメラを設置していた kind: other - secret: true - id: sagara-skipped-round statement: 相良芳江は23時45分の西展示区画の巡回を省略して休憩していた kind: other - secret: true - id: body-found statement: 00時12分、御子柴徹が検疫準備室で江波慎吾の死を発見した kind: observation @@ -97,49 +95,64 @@ facts: timeline: - id: feeder-set at: "23:51" - participants: [morishita] - facts: [feeder-auto-capable, feeding-lamp-motor-linked, morishita-set-auto-feeder] + participants: [ morishita ] + facts: [ feeder-auto-capable, feeding-lamp-motor-linked, morishita-set-auto-feeder ] + record: 給餌設定 description: 森下が給餌装置を四分間隔の自動運転へ切り替える。 + location: 給餌台 - id: first-cycle at: "23:55" participants: [] - facts: [feeder-cycled] + facts: [ feeder-cycled ] + record: 給餌作動記録 description: 自動給餌装置が作動し、展示側から見える青い給餌灯が点く。 + location: 展示側 - id: morishita-leaves at: "23:57" - participants: [morishita] - facts: [morishita-left-station] + participants: [ morishita ] + facts: [ morishita-left-station ] description: 森下が給餌操作台を離れ、バックヤードへ向かう。 + location: 給餌台 - id: sagara-sighting at: "00:02" - participants: [morishita, sagara] - facts: [sagara-saw-morishita] + participants: [ morishita, sagara ] + facts: [ sagara-saw-morishita ] description: 相良が検疫準備室へ続く通路で森下を目撃する。 + location: 通路 - id: enami-death at: "00:05" - participants: [morishita] - facts: [morishita-killed-enami] + participants: [ morishita ] + facts: [ morishita-killed-enami ] description: 森下が検疫準備室で江波を襲い、江波は死亡する。 + location: 検疫準備室 - id: morishita-return at: "00:09" - participants: [morishita] - facts: [morishita-returned] + participants: [ morishita ] + facts: [ morishita-returned ] description: 森下が深海水槽の給餌操作台へ戻る。 + location: 給餌台 - id: discovery at: "00:12" - participants: [mikoshiba, morishita, sakakibara, sagara] - facts: [body-found] + participants: [ mikoshiba, morishita, sakakibara, sagara ] + facts: [ body-found ] description: 御子柴が検疫準備室で江波の死を発見する。 + location: 検疫準備室 characters: - id: morishita name: 森下莉央 - role: suspect publicIntroduction: "生き物の状態変化には鋭いが、責任を問われると防御的になる飼育員。" personality: 生き物の状態変化には鋭いが、責任を問われると防御的になる飼育員。担当展示への愛着が強く、給餌灯を自分のアリバイとして強調する。 goals: - 飼育記録を改ざんしたことを隠したい - 青い給餌灯が点いていた時間は自分も操作台にいたと思わせたい - knowledge: [morishita-aquarist, feeder-auto-capable, feeding-lamp-motor-linked, feeder-cycled, body-found] + knowledge: + [ + morishita-aquarist, + feeder-auto-capable, + feeding-lamp-motor-linked, + feeder-cycled, + body-found + ] secrets: - fact: morishita-falsified-records disclosure: pressured @@ -166,7 +179,6 @@ characters: strategy: maintain-until-contradicted memories: - id: removal-threat - about: enami-planned-report detail: 江波から翌朝に担当を外すと言われ、担当水槽を取り上げられる恐怖が先に立った。 relationships: - character: sakakibara @@ -174,13 +186,18 @@ characters: attitude: 記録の細かさに厳しく、普段から苦手意識がある - id: sakakibara name: 榊原直 - role: suspect publicIntroduction: "几帳面で理屈っぽい獣医師。" personality: 几帳面で理屈っぽい獣医師。在庫管理ミスが表に出るのを恐れているが、給餌設備の基本仕様は把握している。 goals: - 在庫記録を後から修正したことを隠したい - 給餌灯が人の在席を証明しないことは正確に説明する - knowledge: [sakakibara-vet, feeding-lamp-motor-linked, feeder-auto-capable, body-found] + knowledge: + [ + sakakibara-vet, + feeding-lamp-motor-linked, + feeder-auto-capable, + body-found + ] secrets: - fact: sakakibara-hid-stock disclosure: pressured @@ -191,18 +208,22 @@ characters: strategy: maintain-until-contradicted memories: - id: lamp-spec - about: feeding-lamp-motor-linked detail: 青い灯りは操作ボタンではなくモーターの作動信号につながっているので、自動運転でも同じように点くと知っている。 relationships: [] - id: mikoshiba name: 御子柴徹 - role: suspect publicIntroduction: "豪放で口数の多い潜水設備担当。" personality: 豪放で口数の多い潜水設備担当。無断の私物カメラを隠したいが、給餌装置の整備も担当しており機械の挙動には詳しい。 goals: - 私物カメラの無断設置を隠したい - 自動給餌の動作については技術的に正確に答える - knowledge: [mikoshiba-diver, feeder-auto-capable, feeding-lamp-motor-linked, body-found] + knowledge: + [ + mikoshiba-diver, + feeder-auto-capable, + feeding-lamp-motor-linked, + body-found + ] secrets: - fact: mikoshiba-private-camera disclosure: pressured @@ -213,18 +234,16 @@ characters: strategy: maintain-until-contradicted memories: - id: interval-setting - about: morishita-set-auto-feeder detail: 事件の少し前、森下から自動運転の間隔を四分にする設定方法を聞かれた。 relationships: [] - id: sagara name: 相良芳江 - role: witness publicIntroduction: "落ち着いた夜間警備員。" personality: 落ち着いた夜間警備員。巡回を一回省略したことだけは隠したいが、零時すぎにバックヤードで森下とすれ違った記憶は鮮明。 goals: - 西展示区画の巡回を省略したことを隠したい - 00時02分ごろの森下の目撃は正確に話したい - knowledge: [sagara-security, sagara-saw-morishita, feeder-cycled, body-found] + knowledge: [ sagara-security, sagara-saw-morishita, feeder-cycled, body-found ] secrets: - fact: sagara-skipped-round disclosure: pressured @@ -235,7 +254,6 @@ characters: strategy: maintain-until-contradicted memories: - id: backyard-morishita - about: sagara-saw-morishita detail: 00時02分ごろ、給餌中のはずの森下が検疫準備室側から歩いてきたので妙だと思った。 relationships: [] revelations: @@ -250,14 +268,21 @@ revelations: revealCondition: 榊原に青い給餌灯が何を検知して点くのか尋ね、自動運転でも点灯することを確認した。 requires: revelations: [] - evidences: [feeder-controller-log] + evidences: [ feeder-controller-log ] - type: character id: mikoshiba revealCondition: 御子柴に給餌装置の自動運転とランプの連動を尋ね、人の操作と点灯を切り分けた。 requires: revelations: [] - evidences: [feeder-controller-log] - relatedFacts: [feeder-auto-capable, feeding-lamp-motor-linked, morishita-set-auto-feeder, feeder-cycled, morishita-left-station] + evidences: [ feeder-controller-log ] + relatedFacts: + [ + feeder-auto-capable, + feeding-lamp-motor-linked, + morishita-set-auto-feeder, + feeder-cycled, + morishita-left-station + ] - id: morishita-motive title: 翌朝に外される担当 text: 江波は森下の飼育記録改ざんを見つけ、翌朝に館長へ報告して森下を担当展示から外す予定だった。 @@ -268,96 +293,88 @@ revelations: id: morishita revealCondition: 森下に江波が確認していた飼育記録と翌朝の対応を追及し、担当を失う恐れを明確にした。 requires: - revelations: [feeding-lamp-not-presence] - evidences: [husbandry-record-diff] + revelations: [ feeding-lamp-not-presence ] + evidences: [ husbandry-record-diff ] - type: character id: sakakibara revealCondition: 榊原に江波が問題視していた飼育記録を尋ね、館長への報告予定を確認した。 requires: revelations: [] - evidences: [husbandry-record-diff] - relatedFacts: [morishita-falsified-records, enami-found-fraud, enami-planned-report] + evidences: [ husbandry-record-diff ] + relatedFacts: [ morishita-falsified-records, enami-found-fraud, enami-planned-report ] evidences: - id: feeder-controller-log label: 深海水槽の自動給餌設定 description: 23時51分に四分間隔の自動運転へ切り替えられ、指定間隔で給餌装置と青い灯りが動作している。 reveal: - mode: conversation condition: 森下、榊原、御子柴のいずれかに給餌灯の点灯条件と自動運転設定を具体的に尋ねたら開示する。 sources: - { type: character, id: morishita } - { type: character, id: sakakibara } - { type: character, id: mikoshiba } - supports: [feeder-auto-capable, feeding-lamp-motor-linked, morishita-set-auto-feeder, feeder-cycled] - contradicts: ["lie:morishita-feeding-alibi", "lie:morishita-no-auto"] + supports: + [ + feeder-auto-capable, + feeding-lamp-motor-linked, + morishita-set-auto-feeder, + feeder-cycled + ] + contradicts: [ "lie:morishita-feeding-alibi", "lie:morishita-no-auto" ] - id: backyard-sighting label: 零時二分のバックヤード目撃 description: 相良は00時02分ごろ、検疫準備室へ続く通路で森下とすれ違っている。 reveal: - mode: conversation condition: 相良に23時55分から00時05分ごろの巡回経路と会った人物を尋ねたら開示する。 sources: - { type: character, id: sagara } - supports: [sagara-saw-morishita] - contradicts: ["lie:morishita-feeding-alibi"] + supports: [ sagara-saw-morishita ] + contradicts: [ "lie:morishita-feeding-alibi" ] - id: husbandry-record-diff label: 飼育記録の版差分 description: 森下の担当水槽だけ過去の記録と当日の記録で数値が不自然に変わり、江波が翌朝の報告予定を書き残している。 reveal: - mode: conversation - condition: 森下か榊原に江波が事件直前に調べていた飼育記録について尋ね、改ざんの可能性を追及したら開示する。 + condition: 森下か榊原に江波が事件直前に調べていた飼育記録について尋ね、改ざんの可能性を追及したら開示する。または遺体・現場を調べ、「飼育記録の版差分」に関わる資料を確認したら開示する。 sources: - { type: character, id: morishita } - { type: character, id: sakakibara } - supports: [morishita-falsified-records, enami-found-fraud, enami-planned-report] + - { type: victim, id: victim } + supports: [ morishita-falsified-records, enami-found-fraud, enami-planned-report ] contradicts: [] - id: stock-edit-history label: 榊原の在庫修正履歴 description: 榊原が処分予定在庫の記録を後から修正していたことが分かるが、検疫準備室の事件とは別件である。 reveal: - mode: conversation condition: 榊原に在庫記録を後から直していないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: sakakibara } - supports: [sakakibara-hid-stock] - contradicts: ["lie:sakakibara-no-stock-edit"] + supports: [ sakakibara-hid-stock ] + contradicts: [ "lie:sakakibara-no-stock-edit" ] - id: private-camera label: 御子柴の私物カメラ description: 水槽内から御子柴の私物カメラが見つかるが、検疫準備室の事件とは結びつかない。 reveal: - mode: conversation condition: 御子柴に水槽内へ私物機材を置いていないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: mikoshiba } - supports: [mikoshiba-private-camera] - contradicts: ["lie:mikoshiba-no-camera"] + supports: [ mikoshiba-private-camera ] + contradicts: [ "lie:mikoshiba-no-camera" ] - id: skipped-round label: 相良の巡回抜け description: 23時45分の西展示区画の巡回記録だけ位置確認がなく、相良が休憩していたことが分かる。 reveal: - mode: conversation condition: 相良に定時巡回を一度も省略していないか確認し、23時45分の記録を検証したら開示する。 sources: - { type: character, id: sagara } - supports: [sagara-skipped-round] - contradicts: ["lie:sagara-no-skip"] + supports: [ sagara-skipped-round ] + contradicts: [ "lie:sagara-no-skip" ] solution: culprit: morishita summary: 森下は担当水槽の損失を少なく見せるため飼育記録を改ざんしており、江波に見抜かれて翌朝に館長へ報告され、担当から外される予定だった。森下は給餌装置を四分間隔の自動運転へ設定し、青い給餌灯が定期的に点く状況を作った。給餌操作台を離れ、00時02分には相良が検疫準備室側の通路で森下を目撃している。江波を襲った後に操作台へ戻った。灯りは人の操作ではなくモーターの作動に連動していた。 method: 四分間隔の自動給餌で青い給餌灯を点かせ続け、それを在席証明に見せかけて給餌場所を離れた motive: 飼育記録の改ざんが発覚し、翌朝の報告で担当展示と職務上の信用を失うことを恐れたため - requiredFacts: [morishita-falsified-records, enami-planned-report, feeder-auto-capable, feeding-lamp-motor-linked, morishita-set-auto-feeder, morishita-left-station, sagara-saw-morishita, morishita-killed-enami] secretKeywords: - 犯人は森下 - 森下が犯人 - 森下が江波を襲 - 私が江波を襲 - 自動給餌でアリバイを偽装 -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [sakakibara-hid-stock, mikoshiba-private-camera, sagara-skipped-round] - notes: 複数人が見られる青い給餌灯を客観証拠に見せつつ、装置の作動と人の在席を分ける。相良の独立した目撃を合わせて森下へ収束させる。 diff --git a/db/scenarios/storm-hydropower-rising-walkway.yaml b/db/scenarios/storm-hydropower-rising-walkway.yaml index ea82abb..f4bbc0d 100644 --- a/db/scenarios/storm-hydropower-rising-walkway.yaml +++ b/db/scenarios/storm-hydropower-rising-walkway.yaml @@ -1,15 +1,33 @@ schemaVersion: 1 id: storm-hydropower-rising-walkway meta: - title: 山中水力発電所、豪雨の夜 + title: "増水発電所" synopsis: "午後九時十分、豪雨で孤立した山中の水力発電所。その旧制御室で所長・峰岸達也が死亡しているのが発見されました。" category: 不可能犯罪 difficulty: 5 estimatedMinutes: 18 - tags: [発電所, 豪雨, 密室, 水位] victim: name: 峰岸達也 introduction: 山中の水力発電所所長 + foundAt: 21:10 + foundIn: 旧制御室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 峰岸達也は旧制御室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「未実施箇所の点検票」に関わる資料が残されている。 +places: + - id: old-walkway + name: 旧保守歩廊 + shortName: 旧歩廊 + introduction: 排水区画に残る、現在は使われていない保守通路 + situation: 現在は増水で水に覆われ、入口から先へ進めない + findings: + - id: walkway-connects-control + statement: 設備図と入口の表示から、この歩廊は正面扉を通らず旧制御室へ入れる接続口まで続いている。 + - id: reddish-silt-floor + statement: 水際より手前の床には、この歩廊周辺に特有の赤褐色の堆積泥が残っている。 briefing: |- ——事件の記録を読み上げます。 @@ -47,27 +65,21 @@ facts: - id: kido-falsified-inspection statement: 城戸真琴は老朽設備の点検を実施したように記録を改ざんし、未実施のまま済ませていた kind: motive - secret: true - id: minegishi-found-falsification statement: 峰岸達也は事件当日の夕方、城戸真琴の点検記録改ざんを発見した kind: motive - secret: true - id: minegishi-called-kido statement: 20時12分ごろ、峰岸達也は城戸真琴を旧制御室へ呼び、点検記録について説明を求めた kind: motive - secret: true - id: kido-used-walkway statement: 20時18分ごろ、城戸真琴は通行可能だった古い保守歩廊から旧制御室へ入った kind: truth - secret: true - id: kido-killed-minegishi statement: 20時24分ごろ、城戸真琴は旧制御室で峰岸達也を襲い死亡させた kind: truth - secret: true - id: kido-left-before-rise statement: 20時29分ごろ、城戸真琴は古い保守歩廊から旧制御室を離れた kind: truth - secret: true - id: front-door-remained-locked statement: 峰岸達也は旧制御室に入る際に正面扉を内側から施錠しており、その状態は発見まで変わらなかった kind: physical @@ -80,60 +92,81 @@ facts: - id: saionji-secret-draft statement: 西園寺悠は監査報告書の一部を事前に外部へ漏らしていた kind: other - secret: true - id: taniguchi-secret-bypass statement: 谷口航は軽微な警報を面倒がって一時的に非表示にしていた kind: other - secret: true - id: body-found-2110 statement: 21時10分、西園寺悠が旧制御室で峰岸達也の死を発見した kind: observation timeline: - id: records-confrontation at: "20:12" - participants: [kido] - facts: [kido-falsified-inspection, minegishi-found-falsification, minegishi-called-kido] + participants: [ kido ] + facts: + [ + kido-falsified-inspection, + minegishi-found-falsification, + minegishi-called-kido + ] description: 峰岸が城戸を旧制御室へ呼び、未実施点検の記録について説明を求める。 + location: 旧制御室 - id: walkway-still-open at: "20:15" - participants: [taniguchi] - facts: [walkway-passable-2015, old-walkway-connects] + participants: [ taniguchi ] + facts: [ walkway-passable-2015, old-walkway-connects ] + record: 水位記録 description: 水位記録上、古い保守歩廊はまだ通行できる状態にある。 + location: 保守歩廊 - id: kido-enters at: "20:18" - participants: [kido] - facts: [kido-used-walkway] + participants: [ kido ] + facts: [ kido-used-walkway ] description: 城戸が正面扉を使わず、古い保守歩廊から旧制御室へ入る。 + location: 旧制御室 - id: minegishi-death at: "20:24" - participants: [kido] - facts: [kido-killed-minegishi] + participants: [ kido ] + facts: [ kido-killed-minegishi ] description: 城戸が旧制御室で峰岸を襲う。 + location: 旧制御室 - id: kido-leaves at: "20:29" - participants: [kido] - facts: [kido-left-before-rise, kido-boot-silt] + participants: [ kido ] + facts: [ kido-left-before-rise, kido-boot-silt ] + record: 作業靴の泥 description: 城戸が増水前に古い保守歩廊から戻る。 + location: 保守歩廊 - id: water-cuts-route at: "20:40" - participants: [taniguchi] - facts: [walkway-closed-2040, water-log-recorded-rise] + participants: [ taniguchi ] + facts: [ walkway-closed-2040, water-log-recorded-rise ] + record: 水位記録 description: 増水により古い保守歩廊が通行不能となり、発見時の「密室」が完成する。 + location: 保守歩廊 - id: discovery at: "21:10" - participants: [saionji, kido, taniguchi] - facts: [front-door-remained-locked, body-found-2110] + participants: [ saionji, kido, taniguchi ] + facts: [ front-door-remained-locked, body-found-2110 ] + record: 当直日誌 description: 西園寺が旧制御室の正面扉を開けさせ、峰岸の死を発見する。 + location: 旧制御室 characters: - id: kido name: 城戸真琴 - role: suspect publicIntroduction: "古い設備の癖まで覚えている保守主任。" personality: 古い設備の癖まで覚えている保守主任。発見時の歩廊が通れなかった事実を強調し、「あの部屋へは誰も入れない」と言い張る。点検記録の話には神経質になる。 goals: - 点検記録の改ざんを隠したい - 発見時の通行不能を犯行時にも当てはめさせたい - knowledge: [kido-maintenance-chief, old-walkway-connects, walkway-passable-2015, walkway-closed-2040, water-log-recorded-rise, body-found-2110] + knowledge: + [ + kido-maintenance-chief, + old-walkway-connects, + walkway-passable-2015, + walkway-closed-2040, + water-log-recorded-rise, + body-found-2110 + ] secrets: - fact: kido-falsified-inspection disclosure: pressured @@ -158,7 +191,6 @@ characters: strategy: maintain-until-contradicted memories: - id: rising-water - about: walkway-passable-2015 detail: 雨量から見て歩廊が使えなくなるまで少し時間があると分かっていた。古い設備を知る人間なら、その変化を読めた。 relationships: - character: saionji @@ -169,13 +201,18 @@ characters: attitude: 水位ログを見れば時刻の矛盾に気づくかもしれない - id: saionji name: 西園寺悠 - role: witness publicIntroduction: "発電所の安全監査員。" personality: 記録を一つずつ照合する監査員。発見時の現場を見て「密室」と判断したが、その状態がいつ成立したかまでは考えていなかった。報告書漏洩を隠している。 goals: - 監査報告の漏洩を隠したい - 発見時の状況と犯行時の状況を分けて考えたい - knowledge: [saionji-auditor, front-door-remained-locked, walkway-closed-2040, body-found-2110] + knowledge: + [ + saionji-auditor, + front-door-remained-locked, + walkway-closed-2040, + body-found-2110 + ] secrets: - fact: saionji-secret-draft disclosure: pressured @@ -186,7 +223,6 @@ characters: strategy: maintain-until-contradicted memories: - id: sealed-at-discovery - about: walkway-closed-2040 detail: 発見時には水が歩廊を完全に塞いでいたので、最初はその状態がずっと続いていたように感じてしまった。 relationships: - character: kido @@ -197,13 +233,19 @@ characters: attitude: 自動記録については比較的信用している - id: taniguchi name: 谷口航 - role: suspect publicIntroduction: "数値監視を担当する若い運転員。" personality: 数値監視を担当する若い運転員。警報の扱いで規則違反をしたためログの話題を避けるが、水位の上昇時刻については正確に答えられる。 goals: - 警報を非表示にした規則違反を隠したい - 水位がいつ歩廊を塞いだのかを正確に説明したい - knowledge: [taniguchi-operator, walkway-passable-2015, walkway-closed-2040, water-log-recorded-rise, body-found-2110] + knowledge: + [ + taniguchi-operator, + walkway-passable-2015, + walkway-closed-2040, + water-log-recorded-rise, + body-found-2110 + ] secrets: - fact: taniguchi-secret-bypass disclosure: pressured @@ -214,7 +256,6 @@ characters: strategy: maintain-until-contradicted memories: - id: water-curve - about: water-log-recorded-rise detail: 二十時二十分台までは歩廊側の水位が低かったのに、そこから急に上がったグラフが印象に残っている。 relationships: - character: kido @@ -237,14 +278,20 @@ revelations: revealCondition: 谷口に水位記録を時刻ごとに説明してもらい、20時15分には歩廊が通れたと確認したら開示する。 requires: revelations: [] - evidences: [water-level-log] + evidences: [ water-level-log ] - type: character id: saionji revealCondition: 西園寺に発見時の通行不能がいつから続いていたと確認したのか問い、犯行時の状態を未確認だったと整理したら開示する。 requires: revelations: [] - evidences: [water-level-log] - relatedFacts: [old-walkway-connects, walkway-passable-2015, walkway-closed-2040, water-log-recorded-rise] + evidences: [ water-level-log ] + relatedFacts: + [ + old-walkway-connects, + walkway-passable-2015, + walkway-closed-2040, + water-log-recorded-rise + ] - id: inspection-motive title: 未実施点検の記録 text: 峰岸は城戸が未実施の点検を実施済みとしていたことを発見し、事件直前に説明を求めていた。 @@ -257,106 +304,104 @@ revelations: id: kido revealCondition: 城戸に点検票の不一致と峰岸からの呼び出しを具体的に示し、未実施項目を追及したら開示する。 requires: - revelations: [room-became-sealed-later] - evidences: [inspection-sheet] + revelations: [ room-became-sealed-later ] + evidences: [ inspection-sheet ] - type: character id: saionji revealCondition: 西園寺に監査中に見つけた点検記録の不自然さを尋ねたら開示する。 requires: - revelations: [room-became-sealed-later] - evidences: [inspection-sheet] - relatedFacts: [kido-falsified-inspection, minegishi-found-falsification, minegishi-called-kido] + revelations: [ room-became-sealed-later ] + evidences: [ inspection-sheet ] + relatedFacts: + [ + kido-falsified-inspection, + minegishi-found-falsification, + minegishi-called-kido + ] evidences: - id: water-level-log label: 排水区画の水位記録 description: 20時15分時点では歩廊を使える水位で、20時40分ごろに通行不能へ変化したことが分かる。 reveal: - mode: conversation condition: 谷口に旧歩廊付近の水位変化を時刻付きで尋ねるか、西園寺に発見時以前の水位を確認したら開示する。 sources: - type: character id: taniguchi - type: character id: saionji - supports: [walkway-passable-2015, walkway-closed-2040, water-log-recorded-rise] - contradicts: ["lie:kido-impossible-entry"] + supports: [ walkway-passable-2015, walkway-closed-2040, water-log-recorded-rise ] + contradicts: [ "lie:kido-impossible-entry" ] - id: old-walkway-plan label: 旧保守歩廊の接続図 description: 正面扉とは別に、排水区画側から旧制御室へ接続する保守歩廊がある。 reveal: - mode: conversation - condition: 城戸に旧制御室の保守経路を尋ねるか、谷口に設備図上の接続を確認したら開示する。 + condition: 城戸に旧制御室の保守経路を尋ねるか、谷口に設備図上の接続を確認したら開示する。または旧保守歩廊を調べ、旧制御室への接続口を確認したら開示する。 sources: - type: character id: kido - type: character id: taniguchi - supports: [old-walkway-connects] - contradicts: ["lie:kido-impossible-entry"] + - { type: location, id: old-walkway } + supports: [ old-walkway-connects ] + contradicts: [ "lie:kido-impossible-entry" ] - id: reddish-silt label: 城戸の作業靴の赤褐色泥 description: 城戸の作業靴には旧保守歩廊周辺に特徴的な堆積泥が付いている。 reveal: - mode: conversation condition: 城戸に事件前後の移動経路を尋ねるか、谷口に旧歩廊周辺の床の特徴を確認したら開示する。 sources: - type: character id: kido - type: character id: taniguchi - supports: [kido-boot-silt, kido-used-walkway] - contradicts: ["lie:kido-impossible-entry"] + supports: [ kido-boot-silt, kido-used-walkway ] + contradicts: [ "lie:kido-impossible-entry" ] - id: inspection-sheet label: 未実施箇所の点検票 description: 実施済みの印がある項目に対応する現場記録がなく、峰岸が城戸の欄へ確認印を付けている。 reveal: - mode: conversation - condition: 城戸か西園寺に事件当日の監査対象となった点検票について尋ねたら開示する。 + condition: 城戸か西園寺に事件当日の監査対象となった点検票について尋ねたら開示する。または遺体・現場を調べ、「未実施箇所の点検票」に関わる資料を確認したら開示する。 sources: - type: character id: kido - type: character id: saionji - supports: [kido-falsified-inspection, minegishi-found-falsification, minegishi-called-kido] - contradicts: ["lie:kido-inspection-complete"] + - type: victim + id: victim + supports: + [ + kido-falsified-inspection, + minegishi-found-falsification, + minegishi-called-kido + ] + contradicts: [ "lie:kido-inspection-complete" ] - id: leaked-draft label: 監査報告の外部送付記録 description: 西園寺が監査報告の草案を外部へ送った記録。密室の成立時刻とは関係しない。 reveal: - mode: conversation condition: 西園寺に監査内容の外部共有について尋ね、漏洩を否定したら開示する。 sources: - type: character id: saionji - supports: [saionji-secret-draft] - contradicts: ["lie:saionji-no-leak"] + supports: [ saionji-secret-draft ] + contradicts: [ "lie:saionji-no-leak" ] - id: hidden-alarm label: 非表示にされた警報履歴 description: 谷口が軽微な警報を一時的に非表示にしていた履歴。犯行経路とは独立した規則違反である。 reveal: - mode: conversation condition: 谷口に当直中の警報処理を詳しく尋ね、操作を否定したら開示する。 sources: - type: character id: taniguchi - supports: [taniguchi-secret-bypass] - contradicts: ["lie:taniguchi-no-bypass"] + supports: [ taniguchi-secret-bypass ] + contradicts: [ "lie:taniguchi-no-bypass" ] solution: culprit: kido summary: 犯人は城戸真琴。未実施点検の記録改ざんを峰岸に見抜かれ、旧制御室へ呼び出された。城戸は正面扉ではなく、その時点ではまだ通行できた古い保守歩廊を使って旧制御室へ入り、峰岸を襲った後、増水前に同じ経路から戻った。その後の豪雨で歩廊が通れなくなり、発見時には正面扉が内側から施錠された「不可能な部屋」が出来上がった。水位記録は20時15分には歩廊が使え、20時40分ごろに初めて遮断されたことを示す。城戸の靴に旧歩廊特有の泥が付いていることも、その経路の使用を裏付ける。 method: 通行可能なうちに古い保守歩廊から旧制御室へ出入りし、犯行後の増水によって経路が失われた状態を密室だと思わせた motive: 未実施の点検を実施済みとした記録改ざんが発覚し、責任追及を恐れたため - requiredFacts: [kido-falsified-inspection, minegishi-called-kido, walkway-passable-2015, kido-used-walkway, kido-killed-minegishi, kido-left-before-rise, walkway-closed-2040, kido-boot-silt] secretKeywords: - 犯人は城戸 - 城戸が峰岸を襲 - 城戸が旧歩廊を使 - 増水後に密室が完成 -quality: - expectedQuestionCount: - min: 11 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [saionji-secret-draft, taniguchi-secret-bypass] - notes: 密室が犯行前から存在したという暗黙の前提を崩す。発見時の静止画ではなく、環境が変化する時系列を追わせる事件。 diff --git a/db/scenarios/storm-island-scheduled-mail.yaml b/db/scenarios/storm-island-scheduled-mail.yaml index 2fc0a69..aa2de48 100644 --- a/db/scenarios/storm-island-scheduled-mail.yaml +++ b/db/scenarios/storm-island-scheduled-mail.yaml @@ -1,15 +1,24 @@ schemaVersion: 1 id: storm-island-scheduled-mail meta: - title: 研修島青凪荘、暴風の夜 - synopsis: "午後十時二分、沖合の研修施設「青凪荘」の執務室で、奨学財団理事の榊原宗一が死亡しているのが見つかりました。午後八時から暴風で船便が全て欠航し、島に残ったのは榊原を含め五人だけです。" + title: "青凪荘の客" + synopsis: "午後十時二分、沖合の研修施設「青凪荘」の執務室で、奨学財団理事の榊原宗一が死亡しているのが見つかりました。午後八時から暴風で船便が全て欠\ + 航し、島に残ったのは榊原を含め五人だけです。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [離島, 嵐, 予定送信, 死亡時刻] victim: name: 榊原宗一 introduction: 奨学財団理事 + foundAt: 22:02 + foundIn: 執務室 + estimatedDeathAt: "21:28" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 榊原宗一は執務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「相沢の小口支出重複一覧」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +50,12 @@ facts: - id: aizawa-diverted-funds statement: 相沢奈緒は小口支出の名目を使い、複数回にわたって財団資金を私的な支払いへ流用していた kind: motive - secret: true - id: sakakibara-found-diversion statement: 榊原宗一は事件当日、相沢奈緒の小口支出に不自然な重複があることを発見した kind: motive - secret: true - id: sakakibara-planned-audit statement: 榊原宗一は翌朝、相沢奈緒を文書管理から外し、支出記録を監査担当へ提出する予定だった kind: motive - secret: true - id: mail-drafted-1811 statement: 18時11分、榊原宗一は翌朝の資料確認を求めるメール本文を作成した kind: physical @@ -62,18 +68,15 @@ facts: - id: aizawa-knew-scheduled-mail statement: 相沢奈緒は夕方、榊原宗一が21時42分の予定送信を設定する場に同席していた kind: truth - secret: true - id: aizawa-entered-office-2124 statement: 21時24分ごろ、相沢奈緒は榊原宗一の執務室へ入った kind: truth - secret: true - id: horie-saw-aizawa-2129 statement: 21時29分ごろ、堀江充は執務室前の廊下から出てくる相沢奈緒を見た kind: observation - id: aizawa-killed-sakakibara statement: 21時28分ごろ、相沢奈緒は執務室で榊原宗一を襲い死亡させた kind: truth - secret: true - id: aizawa-joined-hatori-2135 statement: 21時35分、相沢奈緒は講義室へ移り、羽鳥栞と翌朝の研修準備を始めた kind: observation @@ -83,15 +86,12 @@ facts: - id: mikage-hid-expense-shift statement: 御影崇は予算超過を隠すため、研修費の一部を別年度の項目へ付け替えていた kind: other - secret: true - id: hatori-shared-materials statement: 羽鳥栞は契約上未公開の研修教材を別の講座で先に使用していた kind: other - secret: true - id: horie-hid-room-use statement: 堀江充は規則に反して空き客室を私的な荷物置き場として使っていた kind: other - secret: true - id: body-found-2202 statement: 22時02分、御影崇が執務室で榊原宗一の死を発見した kind: observation @@ -99,56 +99,75 @@ facts: timeline: - id: mail-drafted at: "18:11" - participants: [aizawa] - facts: [mail-drafted-1811] + participants: [ aizawa ] + facts: [ mail-drafted-1811 ] + record: 作成記録 description: 榊原が翌朝の資料確認を求めるメール本文を作成する。相沢は近くで予定を確認している。 + location: 執務室 - id: mail-scheduled at: "18:12" - participants: [aizawa] - facts: [mail-scheduled-2142, aizawa-knew-scheduled-mail] + participants: [ aizawa ] + facts: [ mail-scheduled-2142, aizawa-knew-scheduled-mail ] + record: 予定送信設定 description: 榊原がメールを21時42分の予定送信へ設定し、相沢もその設定を知る。 + location: 執務室 - id: aizawa-enters-office at: "21:24" - participants: [aizawa] - facts: [aizawa-entered-office-2124] + participants: [ aizawa ] + facts: [ aizawa-entered-office-2124 ] description: 相沢が榊原の執務室へ入る。 + location: 執務室 - id: sakakibara-death at: "21:28" - participants: [aizawa] - facts: [aizawa-killed-sakakibara] + participants: [ aizawa ] + facts: [ aizawa-killed-sakakibara ] description: 相沢が執務室で榊原を襲い、榊原は死亡する。 + location: 執務室 - id: horie-sighting at: "21:29" - participants: [aizawa, horie] - facts: [horie-saw-aizawa-2129] + participants: [ aizawa, horie ] + facts: [ horie-saw-aizawa-2129 ] description: 堀江が執務室前の廊下から出てくる相沢を目撃する。 + location: 廊下 - id: aizawa-joins-hatori at: "21:35" - participants: [aizawa, hatori] - facts: [aizawa-joined-hatori-2135, aizawa-with-hatori-until-2155] + participants: [ aizawa, hatori ] + facts: [ aizawa-joined-hatori-2135, aizawa-with-hatori-until-2155 ] description: 相沢が講義室で羽鳥と翌朝の研修準備を始める。 + location: 講義室 - id: scheduled-mail-sends at: "21:42" participants: [] - facts: [mail-sent-automatically] + facts: [ mail-sent-automatically ] + record: 送信記録 description: 榊原が夕方に設定したメールが、端末への操作なしで自動送信される。 + location: 執務室 - id: discovery at: "22:02" - participants: [mikage, aizawa, hatori, horie] - facts: [body-found-2202] + participants: [ mikage, aizawa, hatori, horie ] + facts: [ body-found-2202 ] description: 御影が執務室で榊原の死を発見する。 + location: 執務室 characters: - id: aizawa name: 相沢奈緒 - role: suspect publicIntroduction: "予定と文書管理に几帳面で、榊原の癖もよく知る秘書。" personality: 予定と文書管理に几帳面で、榊原の癖もよく知る秘書。落ち着いて時系列を整理するように話すが、21時42分のメールを本人の生存証明として意図的に強調する。 goals: - 財団資金を私的に流用したことを隠したい - 21時42分のメールを、榊原がその時刻まで生きていた証拠だと思わせたい - 自分の21時35分以降のアリバイを死亡時刻と重ねたい - knowledge: [aizawa-secretary, mail-drafted-1811, mail-scheduled-2142, mail-sent-automatically, aizawa-joined-hatori-2135, aizawa-with-hatori-until-2155, body-found-2202] + knowledge: + [ + aizawa-secretary, + mail-drafted-1811, + mail-scheduled-2142, + mail-sent-automatically, + aizawa-joined-hatori-2135, + aizawa-with-hatori-until-2155, + body-found-2202 + ] secrets: - fact: aizawa-diverted-funds disclosure: pressured @@ -177,7 +196,6 @@ characters: strategy: maintain-until-contradicted memories: - id: audit-threat - about: sakakibara-planned-audit detail: 榊原から「明朝、支出を監査へ渡す。文書管理からも外れてもらう」と告げられ、過去の流用が全部つながると思った。 relationships: - character: hatori @@ -188,13 +206,12 @@ characters: attitude: 支出の細部まで見られると流用に気づかれるので警戒している - id: mikage name: 御影崇 - role: suspect publicIntroduction: "財団の会計担当。" personality: 数字に細かく、他人の経費処理にも厳しい会計担当。自分も予算項目を付け替えていたため監査の話題を嫌うが、メールの送信情報を見る知識はある。 goals: - 予算項目の付け替えを隠したい - 榊原の監査方針と相沢の支出の不自然さについては必要なら話す - knowledge: [mikage-accountant, mail-sent-automatically, body-found-2202] + knowledge: [ mikage-accountant, mail-sent-automatically, body-found-2202 ] secrets: - fact: mikage-hid-expense-shift disclosure: pressured @@ -205,7 +222,6 @@ characters: strategy: maintain-until-contradicted memories: - id: duplicate-expenses - about: aizawa-diverted-funds detail: 榊原が夕方、相沢の処理した小口支出に同じ金額の重複があると言って一覧を印刷していた。 relationships: - character: aizawa @@ -213,13 +229,18 @@ characters: attitude: 普段は正確だが、最近の小口支出だけ説明が曖昧だと感じている - id: hatori name: 羽鳥栞 - role: witness publicIntroduction: "話し方が明快な外部講師。" personality: 話し方が明快な外部講師。未公開教材を別講座で使ったことは隠したいが、21時35分から55分まで相沢と一緒に準備していたことは正確に証言する。 goals: - 未公開教材を別講座で先に使ったことを隠したい - 21時35分以降の相沢の居場所は正確に話したい - knowledge: [hatori-lecturer, aizawa-joined-hatori-2135, aizawa-with-hatori-until-2155, body-found-2202] + knowledge: + [ + hatori-lecturer, + aizawa-joined-hatori-2135, + aizawa-with-hatori-until-2155, + body-found-2202 + ] secrets: - fact: hatori-shared-materials disclosure: pressured @@ -230,18 +251,16 @@ characters: strategy: maintain-until-contradicted memories: - id: aizawa-arrival - about: aizawa-joined-hatori-2135 detail: 相沢は21時35分ちょうどくらいに講義室へ来た。少し息が上がっていたが、その後21時55分まではずっと一緒だった。 relationships: [] - id: horie name: 堀江充 - role: witness publicIntroduction: "施設内の人の出入りをよく見ている管理担当。" personality: 施設内の人の出入りをよく見ている管理担当。空き客室を私物置き場にしていたことを隠したいが、21時29分に執務室前で見た相沢については時刻も場所も覚えている。 goals: - 空き客室を私物の荷物置き場にしていたことを隠したい - 21時29分に執務室前から出てきた相沢の目撃は正確に話したい - knowledge: [horie-manager, horie-saw-aizawa-2129, body-found-2202] + knowledge: [ horie-manager, horie-saw-aizawa-2129, body-found-2202 ] secrets: - fact: horie-hid-room-use disclosure: pressured @@ -252,8 +271,9 @@ characters: strategy: maintain-until-contradicted memories: - id: office-corridor-aizawa - about: horie-saw-aizawa-2129 detail: 21時29分ごろ、設備盤の時計を確認した直後に、執務室前の廊下から相沢が出てきた。本人は気づかず講義室方向へ急いでいた。 + - id: death-estimate-memory + detail: 発見時に執務室の状態を確認しており、榊原の死亡は21時28分ごろと見積もられるという確認内容を覚えている。 relationships: [] revelations: @@ -268,14 +288,20 @@ revelations: revealCondition: 御影にメールの受信時刻だけでなく、作成時刻や予定送信情報を確認できないか尋ねた。 requires: revelations: [] - evidences: [mail-schedule-metadata] + evidences: [ mail-schedule-metadata ] - type: character id: aizawa revealCondition: 相沢に21時42分のメールが予定送信ではなかったか追及し、夕方の設定情報を突きつけた。 requires: revelations: [] - evidences: [mail-schedule-metadata] - relatedFacts: [mail-drafted-1811, mail-scheduled-2142, mail-sent-automatically, aizawa-knew-scheduled-mail] + evidences: [ mail-schedule-metadata ] + relatedFacts: + [ + mail-drafted-1811, + mail-scheduled-2142, + mail-sent-automatically, + aizawa-knew-scheduled-mail + ] - id: death-window-moves-earlier title: 相沢のアリバイより前の空白 text: 21時42分のメールを生存証明から外すと、相沢が羽鳥と合流する21時35分より前も犯行可能時間に戻る。21時29分には堀江が執務室前から出る相沢を見ている。 @@ -286,15 +312,21 @@ revelations: id: horie revealCondition: 堀江に21時20分から35分ごろ執務室前で見た人物を尋ね、相沢の目撃を具体化した。 requires: - revelations: [mail-was-scheduled] - evidences: [office-corridor-sighting] + revelations: [ mail-was-scheduled ] + evidences: [ office-corridor-sighting ] - type: character id: hatori revealCondition: 羽鳥に相沢と合流した正確な時刻を尋ね、21時35分より前は一緒ではなかったことを確認した。 requires: - revelations: [mail-was-scheduled] - evidences: [office-corridor-sighting] - relatedFacts: [aizawa-entered-office-2124, horie-saw-aizawa-2129, aizawa-joined-hatori-2135, aizawa-with-hatori-until-2155] + revelations: [ mail-was-scheduled ] + evidences: [ office-corridor-sighting ] + relatedFacts: + [ + aizawa-entered-office-2124, + horie-saw-aizawa-2129, + aizawa-joined-hatori-2135, + aizawa-with-hatori-until-2155 + ] - id: aizawa-fund-motive title: 翌朝の支出監査 text: 榊原は相沢の小口支出の重複から資金流用を疑い、翌朝に相沢を文書管理から外して監査担当へ記録を渡す予定だった。 @@ -305,97 +337,110 @@ revelations: id: aizawa revealCondition: 相沢に榊原が調べていた小口支出と翌朝の監査予定を追及し、文書管理から外される恐れを明確にした。 requires: - revelations: [death-window-moves-earlier] - evidences: [expense-duplicate-sheet] + revelations: [ death-window-moves-earlier ] + evidences: [ expense-duplicate-sheet ] - type: character id: mikage revealCondition: 御影に榊原が夕方確認していた重複支出を尋ね、監査予定へ話をつなげた。 requires: revelations: [] - evidences: [expense-duplicate-sheet] - relatedFacts: [aizawa-diverted-funds, sakakibara-found-diversion, sakakibara-planned-audit] + evidences: [ expense-duplicate-sheet ] + relatedFacts: + [ + aizawa-diverted-funds, + sakakibara-found-diversion, + sakakibara-planned-audit + ] evidences: - id: mail-schedule-metadata label: 二十一時四十二分メールの予定送信情報 description: メール本文は18時11分作成、18時12分に21時42分の予定送信へ設定されており、21時42分には端末操作なしで送信されている。 reveal: - mode: conversation condition: 相沢か御影に21時42分のメールについて、受信時刻だけでなく作成時刻と予定送信情報を確認したら開示する。 sources: - { type: character, id: aizawa } - { type: character, id: mikage } - supports: [mail-drafted-1811, mail-scheduled-2142, mail-sent-automatically, aizawa-knew-scheduled-mail] - contradicts: ["lie:aizawa-mail-means-alive", "lie:aizawa-did-not-know-schedule"] + supports: + [ + mail-drafted-1811, + mail-scheduled-2142, + mail-sent-automatically, + aizawa-knew-scheduled-mail + ] + contradicts: [ "lie:aizawa-mail-means-alive", "lie:aizawa-did-not-know-schedule" ] - id: office-corridor-sighting label: 二十一時二十九分の執務室前目撃 description: 堀江は21時29分ごろ、榊原の執務室前の廊下から出てくる相沢を見ている。 reveal: - mode: conversation condition: 堀江に21時20分から35分ごろ執務室前で誰を見たか尋ねたら開示する。 sources: - { type: character, id: horie } - supports: [horie-saw-aizawa-2129, aizawa-entered-office-2124] - contradicts: ["lie:aizawa-no-office-visit"] + supports: [ horie-saw-aizawa-2129, aizawa-entered-office-2124 ] + contradicts: [ "lie:aizawa-no-office-visit" ] - id: expense-duplicate-sheet label: 相沢の小口支出重複一覧 description: 相沢が処理した複数の小口支出に不自然な重複があり、榊原が翌朝の監査提出と担当変更を記している。 reveal: - mode: conversation - condition: 相沢か御影に榊原が夕方確認していた支出一覧を尋ね、重複と翌朝の監査予定を追及したら開示する。 + condition: 相沢か御影に榊原が夕方確認していた支出一覧を尋ね、重複と翌朝の監査予定を追及したら開示する。または遺体・現場を調べ、「相沢の小口支出重複一覧」に関わる資料を確認したら開示する。 sources: - { type: character, id: aizawa } - { type: character, id: mikage } - supports: [aizawa-diverted-funds, sakakibara-found-diversion, sakakibara-planned-audit] + - { type: victim, id: victim } + supports: + [ + aizawa-diverted-funds, + sakakibara-found-diversion, + sakakibara-planned-audit + ] contradicts: [] - id: mikage-budget-sheet label: 御影の年度付け替え表 description: 御影が予算超過を隠すため研修費を別年度へ付け替えていたことが分かるが、榊原の死亡とは独立している。 reveal: - mode: conversation condition: 御影に研修費を別年度へ付け替えていないか尋ね、会計表を検証したら開示する。 sources: - { type: character, id: mikage } - supports: [mikage-hid-expense-shift] - contradicts: ["lie:mikage-no-budget-shift"] + supports: [ mikage-hid-expense-shift ] + contradicts: [ "lie:mikage-no-budget-shift" ] - id: hatori-material-history label: 羽鳥の未公開教材利用履歴 description: 羽鳥が契約上未公開の教材を別講座で先に使用していたことが分かるが、事件とは無関係である。 reveal: - mode: conversation condition: 羽鳥に未公開教材を他の講座で使っていないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: hatori } - supports: [hatori-shared-materials] - contradicts: ["lie:hatori-no-material-share"] + supports: [ hatori-shared-materials ] + contradicts: [ "lie:hatori-no-material-share" ] - id: horie-room-photo label: 空き客室の私物荷物 description: 堀江が規則に反して空き客室を私物置き場にしていたことが分かるが、執務室の事件とは別件である。 reveal: - mode: conversation condition: 堀江に空き客室を私物置き場にしていないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: horie } - supports: [horie-hid-room-use] - contradicts: ["lie:horie-no-private-room"] + supports: [ horie-hid-room-use ] + contradicts: [ "lie:horie-no-private-room" ] + - id: death-estimate + label: 執務室の死亡推定 + description: 執務室の室温と発見時の状態から、榊原の死亡は21時28分ごろと見積もられる。21時42分のメール送信より前である。 + reveal: + condition: 遺体を調べて発見時の状態を確認するか、堀江に執務室へ入ったときの状態と確認内容を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: horie } + supports: [aizawa-killed-sakakibara] + contradicts: [] + revealsDeathTime: true solution: culprit: aizawa summary: 犯人は相沢奈緒。小口支出を使った財団資金の流用を榊原に見抜かれ、翌朝に文書管理から外され監査担当へ記録を渡される予定だった。相沢は榊原が夕方に21時42分の予定送信メールを設定したことを知っていた。21時24分ごろ執務室へ入り、21時28分ごろ榊原を襲い、21時29分には堀江が執務室前から出てくる相沢を目撃している。相沢は21時35分から羽鳥と講義室にいて、その後には確かなアリバイがある。21時42分、榊原が夕方に作成したメールが自動送信され、榊原がその時刻まで生きていたように見えたため、相沢の21時35分以降のアリバイが犯行時間と重なって見えた。 method: 榊原が夕方に設定した21時42分の予定送信メールを本人の生存証明に見せかけ、実際の死亡時刻を自分のアリバイ成立後へずらして見せた motive: 財団資金の流用が発覚し、翌朝の監査で文書管理の立場と信用を失うことを恐れたため - requiredFacts: [aizawa-diverted-funds, sakakibara-planned-audit, mail-drafted-1811, mail-scheduled-2142, mail-sent-automatically, aizawa-knew-scheduled-mail, aizawa-entered-office-2124, horie-saw-aizawa-2129, aizawa-killed-sakakibara, aizawa-joined-hatori-2135] secretKeywords: - 犯人は相沢 - 相沢が犯人 - 相沢が榊原を襲 - 私が榊原を襲 - 予定送信で死亡時刻を偽装 -quality: - expectedQuestionCount: - min: 14 - max: 28 - requiredEvidence: - min: 3 - redHerrings: [mikage-hid-expense-shift, hatori-shared-materials, horie-hid-room-use] - notes: 犯人の在席記録ではなく、被害者の『生存記録』を崩す型。予定送信を見抜くと死亡可能時間が21時35分より前へ戻り、堀江の目撃が急に決定的になる。相沢が予定送信を事前に知っていた点まで確認して偶然利用ではなく意図的な時間偽装へ収束させる。 diff --git a/db/scenarios/storm-lighthouse-relief-slate.yaml b/db/scenarios/storm-lighthouse-relief-slate.yaml index f774b10..9f9089f 100644 --- a/db/scenarios/storm-lighthouse-relief-slate.yaml +++ b/db/scenarios/storm-lighthouse-relief-slate.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: storm-lighthouse-relief-slate meta: - title: 夕凪灯台、暴風の夜 + title: "夕凪という名の嵐" synopsis: "午後八時五十分、暴風で補給船の接岸が不可能となった夕凪灯台の整備室で、主任灯台守の倉橋徹が死亡しているのが見つかりました。島にいるのは当直員の鳥越玲、通信担当の橋場圭、補助員の森下透の三人だけです。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [灯台, 暴風, 記録, 時刻] victim: name: 倉橋徹 introduction: 夕凪灯台主任灯台守 + foundAt: 20:50 + foundIn: 整備室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 倉橋徹は整備室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「補給報告の在庫差メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -50,38 +57,30 @@ facts: - id: torigoe-copied-panel-reading statement: 鳥越玲は当直板の西設備欄に、制御盤に残っていた20時12分の自動計測値を転記した kind: truth - secret: true - id: torigoe-coat-wet-before statement: 鳥越玲の外套は19時台の東側巡回で既に濡れていた kind: truth - secret: true - id: hashiba-saw-torigoe-2021 statement: 20時21分ごろ、橋場圭は整備室近くの内階段で鳥越玲とすれ違った kind: observation - id: kurahashi-death-2024 statement: 20時24分ごろ、倉橋徹は整備室で襲われ死亡した kind: truth - secret: true - id: torigoe-killed-kurahashi statement: 鳥越玲は20時24分ごろ整備室で倉橋徹を襲い死亡させた kind: truth - secret: true - id: fuel-report-next-day statement: 倉橋徹は翌日の補給報告で、燃料在庫の不自然な不足を本部へ報告する予定だった kind: motive - secret: true - id: torigoe-hid-fuel-shortage statement: 鳥越玲は自分の記録ミスによる燃料在庫の不足を帳簿上で先送りしていた kind: motive - secret: true - id: hashiba-private-channel statement: 橋場圭は勤務中、規定外の個人的な無線受信をしていた kind: other - secret: true - id: morishita-slept-on-duty statement: 森下透は発電室当番中に短時間眠っていた kind: other - secret: true - id: body-found-2050 statement: 20時50分、森下透が整備室で倉橋徹の死を発見した kind: observation @@ -90,36 +89,50 @@ timeline: at: "18:40" participants: [torigoe, morishita] facts: [west-gate-sealed] + record: 封鎖確認票 description: 暴風警戒により西側外扉が封鎖される。 + location: 西外扉 - id: east-round at: "19:25" participants: [torigoe] facts: [torigoe-coat-wet-before] description: 鳥越が東側巡回を行い、外套が雨で濡れる。 + location: 東側 - id: panel-reading at: "20:12" participants: [] facts: [slate-entry-2012, slate-written-after-round, torigoe-copied-panel-reading] + record: 計測記録 description: 西設備の自動計測値が制御盤に記録され、後に鳥越がその時刻を当直板へ転記する。 + location: 制御盤 - id: stair-sighting at: "20:21" participants: [torigoe, hashiba] facts: [hashiba-saw-torigoe-2021] description: 橋場が整備室近くの内階段で鳥越とすれ違う。 + location: 内階段 - id: kurahashi-death at: "20:24" participants: [torigoe] facts: [kurahashi-death-2024, torigoe-killed-kurahashi] description: 鳥越が整備室で倉橋を襲い、倉橋は死亡する。 + location: 灯台内 - id: discovery at: "20:50" participants: [morishita, torigoe, hashiba] - facts: [body-found-2050, west-seal-intact] - description: 森下が倉橋の死を発見し、西側外扉の封鎖も保たれていることが確認される。 + facts: [body-found-2050] + description: 森下が整備室で倉橋の死を発見する。 + location: 整備室 + - id: west-seal-checked + at: "20:50" + participants: [morishita] + facts: [west-seal-intact] + record: 封鎖確認票 + description: 森下が西側外扉を確認し、封鎖確認票が保たれていることを確かめる。 + location: 西外扉 characters: - id: torigoe name: 鳥越玲 - role: suspect publicIntroduction: "実直に見える設備当直員。" personality: 実直に見える設備当直員。手順や記録を重視する話し方をし、数字を示して自分の説明を固めようとする。記録ミスを倉橋に見つけられたことを強く恐れている。 goals: @@ -148,10 +161,8 @@ characters: strategy: maintain-until-contradicted memories: - id: fuel-warning - about: fuel-report-next-day detail: 倉橋から「明日の補給報告では数字を直さず、そのまま本部へ出す」と告げられ、記録ミスが表に出ると悟った。 - id: slate-habit - about: slate-written-after-round detail: 当直板は巡回から戻ってまとめて書くのが昔からの慣例で、記載時刻が筆記時刻ではないことを当然のように知っている。 relationships: - character: hashiba @@ -162,7 +173,6 @@ characters: attitude: 注意力が散漫だと思っている - id: hashiba name: 橋場圭 - role: witness publicIntroduction: "灯台の通信担当。" personality: 観察力のある通信担当。時刻は通信時計で確認する癖がある一方、規定外の個人的な受信をしていたため通信記録を調べられるのを嫌がる。 goals: @@ -179,7 +189,6 @@ characters: strategy: maintain-until-contradicted memories: - id: stair-clock - about: hashiba-saw-torigoe-2021 detail: 定時連絡の一分後に通信室を出たところで鳥越とすれ違ったので、20時21分だったことには自信がある。 relationships: - character: torigoe @@ -187,7 +196,6 @@ characters: attitude: 記録を盾にすると急に頑固になると思っている - id: morishita name: 森下透 - role: witness publicIntroduction: "若い補助員。" personality: 若い補助員。素直だが自分の勤務態度に自信がなく、居眠りを知られるのを恐れている。設備の封鎖確認には橋場より詳しい。 goals: @@ -204,7 +212,6 @@ characters: strategy: maintain-until-contradicted memories: - id: intact-seal - about: west-seal-intact detail: 発見後に西側外扉を確認したとき、夕方に自分で貼った封鎖確認票が同じ位置のまま残っていた。 relationships: - character: torigoe @@ -252,7 +259,6 @@ evidences: label: 西側外扉の封鎖確認票 description: 18時40分に封鎖された確認票が事件後も切れておらず、20時台に西側へ出たという説明と両立しない。 reveal: - mode: conversation condition: 森下か鳥越に暴風時の西側外扉の運用と事件後の状態を尋ねたら開示する。 sources: - type: character @@ -265,7 +271,6 @@ evidences: label: 当直板の記入手順 description: 点検時刻は現場で書くのではなく、巡回後に制御盤の表示やメモを見てまとめて転記する運用だった。 reveal: - mode: conversation condition: 橋場か鳥越に当直板をいつどこで書くのか具体的に尋ねたら開示する。 sources: - type: character @@ -278,7 +283,6 @@ evidences: label: 外套の先行使用記録 description: 鳥越の外套は19時台の東側巡回で既に濡れており、20時台に西側へ出た証明にはならない。 reveal: - mode: conversation condition: 鳥越に外套がいつから濡れていたか尋ねるか、森下に19時台の巡回について確認したら開示する。 sources: - type: character @@ -291,7 +295,6 @@ evidences: label: 二十時二十一分の内階段目撃 description: 橋場は通信時計を確認した直後、整備室近くの内階段で鳥越とすれ違っている。 reveal: - mode: conversation condition: 橋場に20時20分前後の移動と誰に会ったか尋ねたら開示する。 sources: - type: character @@ -302,20 +305,20 @@ evidences: label: 補給報告の在庫差メモ description: 倉橋の翌日提出予定資料に、鳥越の担当分だけ燃料在庫の差を本部へ報告する注記がある。 reveal: - mode: conversation - condition: 鳥越か森下に翌日の補給報告と燃料在庫の差について尋ねたら開示する。 + condition: 鳥越か森下に翌日の補給報告と燃料在庫の差について尋ねたら開示する。または遺体・現場を調べ、「補給報告の在庫差メモ」に関わる資料を確認したら開示する。 sources: - type: character id: torigoe - type: character id: morishita + - type: victim + id: victim supports: [torigoe-hid-fuel-shortage, fuel-report-next-day] contradicts: [] - id: private-radio-log label: 規定外の受信履歴 description: 橋場が個人的な受信をしていたことが分かるが、整備室の事件とは結びつかない。 reveal: - mode: conversation condition: 橋場に業務外の無線受信について具体的に尋ねたら開示する。 sources: - type: character @@ -326,7 +329,6 @@ evidences: label: 発電室の巡回空白 description: 森下が短時間眠っていたことが分かるが、西側外扉の封鎖確認や鳥越の位置とは独立している。 reveal: - mode: conversation condition: 森下に発電室で記録が途切れた時間を追及したら開示する。 sources: - type: character @@ -338,18 +340,9 @@ solution: summary: 犯人は鳥越玲。当直板の「20:12 西設備確認」は現場でその時刻に書かれた記録ではなく、巡回後に制御盤の自動計測値などをまとめて転記する運用だった。しかも西側外扉の封鎖確認票は事件後も切れておらず、鳥越が20時台に西側へ出たという説明は成立しない。濡れた外套も19時台の東側巡回で既に濡れていた。20時21分には橋場が整備室近くの内階段で鳥越を目撃しており、鳥越は20時24分ごろ倉橋を襲った。倉橋は翌日、鳥越が先送りしていた燃料在庫の不足を本部へ報告する予定だった。当直板、外套、時刻という三つの一見独立した証拠が、実際にはどれも鳥越の西側滞在を証明していないことが核心である。 method: 制御盤の過去の計測時刻を当直板へ転記し、以前から濡れていた外套も利用して西側巡回のアリバイを作ったうえで、整備室で倉橋を襲った motive: 翌日の本部報告で、自分が先送りしていた燃料在庫の不足と記録ミスが明るみに出るのを恐れたため - requiredFacts: [west-seal-intact, slate-written-after-round, torigoe-copied-panel-reading, torigoe-coat-wet-before, hashiba-saw-torigoe-2021, fuel-report-next-day, torigoe-killed-kurahashi] secretKeywords: - 犯人は鳥越 - 鳥越が犯人 - 鳥越が倉橋を襲 - 私が倉橋を襲 - 当直板でアリバイを偽装 -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [hashiba-private-channel, morishita-slept-on-duty] - notes: 灯台の記録と残置物から時刻を推定する実在の孤立事件の雰囲気を抽象化。複数のアリバイ材料が同じ誤った前提に依存していることを見抜く構造。 diff --git a/db/scenarios/storm-mountain-inn-echoed-cane.yaml b/db/scenarios/storm-mountain-inn-echoed-cane.yaml index bcd62a2..66f6223 100644 --- a/db/scenarios/storm-mountain-inn-echoed-cane.yaml +++ b/db/scenarios/storm-mountain-inn-echoed-cane.yaml @@ -1,15 +1,34 @@ schemaVersion: 1 id: storm-mountain-inn-echoed-cane meta: - title: 山宿梢庵、暴風雨の夜 + title: "雨の梢庵でお待ちください" synopsis: "午後九時五十分、暴風雨で孤立した山宿「梢庵」で、主人・桐谷宗介が帳場奥の事務室で死亡しているのが見つかりました。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [山宿, 暴風雨, 音, 目撃錯誤] victim: name: 桐谷宗介 introduction: 山宿「梢庵」主人 + foundAt: 21:50 + foundIn: 帳場奥 + estimatedDeathAt: "21:08" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 桐谷宗介は帳場奥で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「不自然な仕入れ伝票」に関わる資料が残されている。 +places: + - id: old-stairs + name: 厨房脇の旧階段 + shortName: 旧階段 + introduction: 厨房脇に残る、普段使われない古い木造階段 + situation: 古い木造の隔壁と手すりがそのまま残されている + findings: + - id: partition-carries-taps + statement: 手すりを軽く叩くと、木の中空隔壁を通って二階廊下側へ乾いた音がよく響く。 + - id: cane-height-marks + statement: 隔壁には、桐谷の杖の金属部分と高さの合う新しい打痕が残っている。 briefing: |- ——事件の記録を読み上げます。 @@ -50,27 +69,21 @@ facts: - id: supplier-kickback statement: 秋庭澄は食材業者から不正な謝礼を受け取り、割高な仕入れを続けていた kind: motive - secret: true - id: kiritani-found-kickback statement: 桐谷宗介は事件当日の夜、仕入れ伝票から秋庭澄と業者の不自然な取引に気づいた kind: motive - secret: true - id: kiritani-confronted-akiwa statement: 20時58分ごろ、桐谷宗介は秋庭澄に翌朝仕入れ業者との契約を見直すと告げた kind: motive - secret: true - id: akiwa-killed-kiritani statement: 21時08分ごろ、秋庭澄は帳場奥の事務室で桐谷宗介を襲い死亡させた kind: truth - secret: true - id: akiwa-took-cane statement: 秋庭澄は犯行後、桐谷宗介の杖を事務室から厨房脇へ持ち出した kind: truth - secret: true - id: akiwa-made-tapping statement: 21時25分ごろ、秋庭澄は厨房脇の旧階段で桐谷宗介の杖を木製隔壁に当て、二階から聞こえるような連続音を生じさせた kind: truth - secret: true - id: cane-found-kitchen statement: 事件発見直前、桐谷宗介の杖は厨房脇の物入れで見つかった kind: physical @@ -80,11 +93,9 @@ facts: - id: morisaki-secret-unpaid statement: 森崎透は長期滞在分の宿泊代を一部未払いのままにしていた kind: other - secret: true - id: sakakibara-secret-materials statement: 榊原蓮は修繕用の余剰木材を無断で持ち帰るつもりだった kind: other - secret: true - id: body-found-2150 statement: 21時50分、榊原蓮が帳場奥で桐谷宗介の死を発見した kind: observation @@ -94,30 +105,42 @@ timeline: participants: [akiwa] facts: [supplier-kickback, kiritani-found-kickback, kiritani-confronted-akiwa] description: 桐谷が秋庭に不自然な仕入れを問い、翌朝に契約を見直すと告げる。 + location: 山宿内 - id: kiritani-death at: "21:08" participants: [akiwa] facts: [akiwa-killed-kiritani, akiwa-took-cane] description: 秋庭が帳場奥で桐谷を襲い、愛用の杖を持ち出す。 + location: 帳場奥 - id: tapping-staged at: "21:25" - participants: [akiwa, morisaki] + participants: [akiwa] + witnesses: [morisaki] facts: [akiwa-made-tapping, morisaki-heard-taps, morisaki-heard-no-steps] description: 秋庭が旧階段で杖の音を作り、森崎は二階廊下を桐谷が歩いていると思い込む。 + location: 旧階段 + - id: morisaki-hears-tapping + at: "21:25" + participants: [morisaki] + facts: [morisaki-heard-taps, morisaki-heard-no-steps] + description: 森崎は二階廊下で杖に似た音を聞くが、足音は聞いていない。 + location: 二階廊下 - id: cane-hidden at: "21:31" participants: [akiwa] facts: [cane-found-kitchen, rail-fresh-marks] + record: 物入れの杖 description: 秋庭が杖を厨房脇の物入れへ戻し、旧階段には新しい打痕が残る。 + location: 厨房脇 - id: discovery at: "21:50" participants: [sakakibara, akiwa, morisaki] facts: [body-found-2150] description: 榊原が帳場奥で桐谷の死を発見する。 + location: 帳場奥 characters: - id: akiwa name: 秋庭澄 - role: suspect publicIntroduction: "穏やかな料理長で、客の好みをよく覚えている。" personality: 穏やかな料理長で、客の好みをよく覚えている。厨房と旧階段を自由に使える。九時二十五分の杖音を桐谷本人の行動と断定し、仕入れの話を避けようとする。 goals: @@ -148,7 +171,6 @@ characters: strategy: maintain-until-contradicted memories: - id: sound-carries - about: old-partition-transmits detail: 厨房の鍋を旧階段の壁へぶつけたとき、二階から客が降りてきたことがあり、音が上へ抜けるのを知っていた。 relationships: - character: morisaki @@ -159,7 +181,6 @@ characters: attitude: 壁の音の伝わり方に気づかれる可能性があり警戒している - id: morisaki name: 森崎透 - role: witness publicIntroduction: "山宿の常連客。" personality: 桐谷と長い付き合いがあり、杖の音を何度も聞いてきた常連客。その親しさゆえに、特徴的な音を聞いた瞬間に桐谷本人だと決めつけた。宿泊代の未払いを隠している。 goals: @@ -176,7 +197,6 @@ characters: strategy: maintain-until-contradicted memories: - id: taps-only - about: morisaki-heard-no-steps detail: 聞こえたのは規則的な金属音だけだった。あとから考えると、歩くなら一緒にするはずの床鳴りや足音は記憶にない。 relationships: - character: akiwa @@ -187,7 +207,6 @@ characters: attitude: 建物の音については自分より詳しい - id: sakakibara name: 榊原蓮 - role: suspect publicIntroduction: "古い木造建築の癖を体で覚えている大工。" personality: 古い木造建築の癖を体で覚えている大工。余剰材料を持ち帰ろうとした後ろめたさはあるが、音の伝わり方については具体的に説明できる。 goals: @@ -204,8 +223,9 @@ characters: strategy: maintain-until-contradicted memories: - id: hollow-wall - about: old-partition-transmits detail: 旧階段と二階北廊下の間は昔の中空壁が連続していて、硬い音だけが意外なほど遠くへ抜ける。 + - id: death-estimate-memory + detail: 発見時の帳場奥の状態を確認しており、桐谷の死亡は21時08分ごろと見積もられるという確認内容を覚えている。 relationships: - character: akiwa relation: 宿スタッフ @@ -261,7 +281,6 @@ evidences: label: 森崎の音の聞き分け description: 森崎は特徴的な金属音だけを聞き、桐谷の足音や声は確認していない。 reveal: - mode: conversation condition: 森崎に九時二十五分の音を細かく再現してもらい、足音や声の有無を問い直したら開示する。 sources: - type: character @@ -272,20 +291,19 @@ evidences: label: 旧階段と二階廊下の中空壁 description: 二つの区画は同じ古い中空隔壁に接し、旧階段側の硬い音が二階から聞こえることがある。 reveal: - mode: conversation - condition: 榊原に古い建物の音の伝わり方を尋ねるか、秋庭に旧階段で音が響くことを知っていたか確認したら開示する。 + condition: 榊原に古い建物の音の伝わり方を尋ねるか、秋庭に旧階段で音が響くことを知っていたか確認したら開示する。または厨房脇の旧階段を調べ、隔壁の構造と音の伝わり方を確認したら開示する。 sources: - type: character id: sakakibara - type: character id: akiwa + - { type: location, id: old-stairs } supports: [old-partition-transmits] contradicts: ["lie:akiwa-cane-proves-alive"] - id: cane-kitchen label: 厨房脇で見つかった杖 description: 桐谷の杖は発見直前、本人のいる帳場奥ではなく厨房脇の物入れで見つかった。 reveal: - mode: conversation condition: 秋庭か榊原に桐谷の杖がどこで見つかったか尋ねたら開示する。 sources: - type: character @@ -298,7 +316,6 @@ evidences: label: 旧階段の新しい打痕 description: 木製隔壁には杖の金属部分と高さの合う新しい打痕が複数残っている。 reveal: - mode: conversation condition: 榊原に旧階段の新しい傷を尋ねるか、秋庭に杖を厨房側へ持ち込んだ可能性を追及したら開示する。 sources: - type: character @@ -311,20 +328,20 @@ evidences: label: 不自然な仕入れ伝票 description: 同じ業者から相場より高い価格で仕入れた記録と、秋庭が処理した伝票がまとまっている。 reveal: - mode: conversation - condition: 秋庭に仕入れ業者との関係を尋ねるか、桐谷が事件前に確認していた伝票について追及したら開示する。 + condition: 秋庭に仕入れ業者との関係を尋ねるか、桐谷が事件前に確認していた伝票について追及したら開示する。または遺体・現場を調べ、「不自然な仕入れ伝票」に関わる資料を確認したら開示する。 sources: - type: character id: akiwa - type: character id: morisaki + - type: victim + id: victim supports: [supplier-kickback, kiritani-found-kickback, kiritani-confronted-akiwa] contradicts: ["lie:akiwa-clean-supplies"] - id: unpaid-bill label: 森崎の未払い宿泊票 description: 森崎には以前からの宿泊代の未払いがあるが、杖音の偽装とは結びつかない。 reveal: - mode: conversation condition: 森崎に桐谷との金銭関係を尋ね、未払いを否定したら開示する。 sources: - type: character @@ -335,29 +352,30 @@ evidences: label: 持ち出し予定の余剰木材 description: 榊原が余剰木材を無断で持ち帰ろうとしていたことが分かるが、事件とは独立した秘密である。 reveal: - mode: conversation condition: 榊原に余剰材料の扱いを尋ね、持ち帰りを否定したら開示する。 sources: - type: character id: sakakibara supports: [sakakibara-secret-materials] contradicts: ["lie:sakakibara-no-materials"] + - id: death-estimate + label: 帳場奥の死亡推定 + description: 帳場奥の室温と発見時の状態から、桐谷の死亡は21時08分ごろと見積もられる。21時25分に聞かれた杖音より前である。 + reveal: + condition: 遺体を調べて発見時の状態を確認するか、榊原に発見時の室内と確認内容を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: sakakibara } + supports: [akiwa-killed-kiritani] + contradicts: [] + revealsDeathTime: true solution: culprit: akiwa summary: 犯人は秋庭澄。仕入れ業者から謝礼を受けていたことを桐谷に見抜かれ、翌朝の契約見直しを告げられたため桐谷を襲った。秋庭は桐谷の杖を厨房脇へ持ち出し、旧階段の中空壁へ金属部分を当てて特徴的な音を作った。森崎は長年聞き慣れた音だったため二階を桐谷が歩いていると思ったが、実際には足音も声も確認していない。杖が厨房脇で見つかったこと、旧階段に新しい打痕があること、建物の構造上そこから二階へ硬い音が伝わることを合わせると、九時二十五分の音は桐谷の生存証明ではない。 method: 桐谷を襲った後、愛用の杖と建物の音の伝わり方を利用して二階を歩く杖音を装い、死亡時刻を遅く見せた motive: 業者からの不正な謝礼と割高な仕入れが発覚し、契約見直しで不正が表面化することを恐れたため - requiredFacts: [supplier-kickback, kiritani-confronted-akiwa, akiwa-killed-kiritani, akiwa-took-cane, old-partition-transmits, morisaki-heard-no-steps, akiwa-made-tapping, rail-fresh-marks] secretKeywords: - 犯人は秋庭 - 秋庭が桐谷を襲 - 秋庭が杖の音を作 - 杖音で死亡時刻を偽装 -quality: - expectedQuestionCount: - min: 10 - max: 22 - requiredEvidence: - min: 3 - redHerrings: [morisaki-secret-unpaid, sakakibara-secret-materials] - notes: 特徴的な音を人物識別に使う危うさを問う。録音や電子機器ではなく、建物の音響と物品の移動を組み合わせる。 diff --git a/db/scenarios/storm-planetarium-reflected-witness.yaml b/db/scenarios/storm-planetarium-reflected-witness.yaml index a62f916..8d63211 100644 --- a/db/scenarios/storm-planetarium-reflected-witness.yaml +++ b/db/scenarios/storm-planetarium-reflected-witness.yaml @@ -1,15 +1,34 @@ schemaVersion: 1 id: storm-planetarium-reflected-witness meta: - title: 星見ヶ丘天象館、山頂の夜 + title: "星見ヶ丘は今夜も曇り" synopsis: "午後十時、暴風で山道とロープウェイが停止した星見ヶ丘天象館で、館長の犬塚誠が投影準備室内で死亡しているのが見つかりました。館内に残っていたのは展示担当の小野寺莉香、投影技師の松田圭介、売店責任者の朝倉葉月の三人だけです。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [天象館, 反射, 目撃証言, 死亡時刻] victim: name: 犬塚誠 introduction: 星見ヶ丘天象館館長 + foundAt: 22:00 + foundIn: 投影準備室 + estimatedDeathAt: "21:24" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 犬塚誠は投影準備室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「運営法人への予算報告草案」に関わる資料が残されている。 +places: + - id: observation-glass + name: 投影室の観察ガラス + shortName: 観察ガラス + introduction: 投影室と廊下を隔てる、大型の観察窓 + situation: 投影室と廊下のあいだを、大きな一枚ガラスが隔てている + findings: + - id: corridor-reflection + statement: 投影室を暗くして廊下側を明るくすると、ガラスには廊下側に立つ人物の像が強く映り込む。 + - id: witness-position-replay + statement: 21時35分の立ち位置を再現すると、廊下にいた人物の像が投影室内の人影のように重なる。 briefing: |- ——事件の記録を読み上げます。 @@ -47,45 +66,36 @@ facts: - id: onodera-behind-matsuda-2135 statement: 21時35分、松田圭介が観察ガラスを見たとき、小野寺莉香は白い展示用上着を着て松田の数歩後ろに立っていた kind: observation - secret: true - id: matsuda-saw-white-figure statement: 21時35分、松田圭介は観察ガラスに白い展示用上着の人物像を見て、投影室内の犬塚誠だと思った kind: observation - id: figure-was-reflection statement: 21時35分に松田圭介が観察ガラスで見た白い人物像は、投影室内の犬塚誠ではなく、廊下側にいた小野寺莉香の反射像だった kind: truth - secret: true - id: onodera-knew-reflection statement: 小野寺莉香は展示導線設計時の検証で、暗い投影室の観察ガラスが廊下側を強く反射することを知っていた kind: truth - secret: true - id: inuzuka-death-2124 statement: 21時24分ごろ、犬塚誠は投影準備室で襲われ死亡した kind: truth - secret: true - id: onodera-killed-inuzuka statement: 小野寺莉香は21時24分ごろ投影準備室で犬塚誠を襲い死亡させた kind: truth - secret: true - id: onodera-endorsed-sighting statement: 小野寺莉香は松田圭介の21時35分の目撃に対し、「館長はまだ投影室にいた」と同意した kind: testimony - id: exhibit-budget-found statement: 犬塚誠は小野寺莉香が展示予算の一部を正式承認なしに別企画へ振り替えていたことに気づいた kind: motive - secret: true - id: budget-report-next-day statement: 犬塚誠は翌朝、小野寺莉香の無断予算振替を運営法人へ報告する予定だった kind: motive - secret: true - id: matsuda-hid-test-failure statement: 松田圭介は投影機器の事前テストを一部省略していた kind: other - secret: true - id: asakura-hid-stock-loss statement: 朝倉葉月は売店在庫の不足を自費補填して帳簿上隠していた kind: other - secret: true - id: body-found-2200 statement: 22時00分、朝倉葉月が投影準備室で犬塚誠の死を発見した kind: observation @@ -95,25 +105,29 @@ timeline: participants: [onodera] facts: [inuzuka-death-2124, onodera-killed-inuzuka] description: 小野寺が投影準備室で犬塚を襲い、犬塚は死亡する。 + location: 投影準備室 - id: booth-lights-down at: "21:30" participants: [matsuda] facts: [booth-dark-2130, glass-reflects-dark-booth] + record: 点検表 description: 機器点検のため投影室内の主照明が落とされ、観察ガラスが廊下側を反射しやすい状態になる。 + location: 投影室 - id: reflected-sighting at: "21:35" participants: [onodera, matsuda] facts: [onodera-behind-matsuda-2135, matsuda-saw-white-figure, figure-was-reflection, onodera-endorsed-sighting] description: 松田が観察ガラスに映った白い人物像を犬塚だと思い込み、小野寺がその誤認を訂正せず生存証言として補強する。 + location: 天象館内 - id: discovery at: "22:00" participants: [asakura, onodera, matsuda] facts: [body-found-2200] description: 朝倉が投影準備室で犬塚の死を発見する。 + location: 投影準備室 characters: - id: onodera name: 小野寺莉香 - role: suspect publicIntroduction: "空間演出に詳しく、人の視線や展示の見え方を意識する企画担当。" personality: 空間演出に詳しく、人の視線や展示の見え方を意識する企画担当。落ち着いて松田の目撃を肯定するが、ガラスの反射条件を誰より理解している。 goals: @@ -142,10 +156,8 @@ characters: strategy: maintain-until-contradicted memories: - id: reflection-test - about: onodera-knew-reflection detail: 展示導線の確認で、投影室を暗くすると観察ガラスに廊下側の人影が鏡のように映ることを何度も試している。 - id: budget-warning - about: budget-report-next-day detail: 犬塚から「明日、法人に振替の経緯を報告する」と言われ、企画担当を外されると感じた。 relationships: - character: matsuda @@ -156,7 +168,6 @@ characters: attitude: 閉館後の館内を歩き回るため目撃を警戒している - id: matsuda name: 松田圭介 - role: witness publicIntroduction: "天象館の投影技師。" personality: 機械には強いが、視覚的な思い込みには無自覚な投影技師。白い上着を見て犬塚だと判断したが、顔までは確認していない。機器テストを省略したことを隠したい。 goals: @@ -173,18 +184,17 @@ characters: strategy: maintain-until-contradicted memories: - id: saw-only-white-jacket - about: matsuda-saw-white-figure detail: ガラス越しに白い上着の上半身が見えたので犬塚だと思ったが、顔をはっきり見たわけではない。 - id: onodera-behind - about: onodera-behind-matsuda-2135 detail: 「館長、まだ中にいるんですね」と言ったとき、小野寺がすぐ後ろから「そうみたいですね」と返事をしたのを覚えている。 + - id: death-estimate-memory + detail: 発見時の投影準備室の状態を確認しており、犬塚の死亡は21時24分ごろと見積もられるという確認内容を覚えている。 relationships: - character: onodera relation: 同僚 attitude: ガラスの見え方は自分より小野寺のほうが詳しいと認識している - id: asakura name: 朝倉葉月 - role: witness publicIntroduction: "現実的な売店責任者で、人の服装をよく覚えている。" personality: 現実的な売店責任者で、人の服装をよく覚えている。閉館作業中に小野寺が白い展示用上着を着ていたことを見ているが、在庫不足を隠している。 goals: @@ -201,7 +211,6 @@ characters: strategy: maintain-until-contradicted memories: - id: onodera-white-jacket - about: white-jackets-similar detail: 閉館後、小野寺が犬塚と同じ型の白い展示用上着を着ているのを見た。 relationships: - character: onodera @@ -249,20 +258,19 @@ evidences: label: 観察ガラスの照明条件テスト description: 投影室を暗くして廊下を明るくすると、観察ガラスには室内より廊下側の人物が強く反射する。 reveal: - mode: conversation - condition: 松田か小野寺に投影室消灯時の観察ガラスの見え方を具体的に尋ねたら開示する。 + condition: 松田か小野寺に投影室消灯時の観察ガラスの見え方を具体的に尋ねたら開示する。または投影室の観察ガラスを調べ、消灯時の反射を同じ立ち位置で再現したら開示する。 sources: - type: character id: matsuda - type: character id: onodera + - { type: location, id: observation-glass } supports: [booth-dark-2130, glass-reflects-dark-booth, onodera-knew-reflection] contradicts: ["lie:onodera-sighting-was-inuzuka"] - id: white-jacket-record label: 閉館作業時の白い上着 description: 犬塚だけでなく小野寺も同型の白い展示用上着を着ていたことが確認できる。 reveal: - mode: conversation condition: 朝倉か松田に閉館作業中の小野寺と犬塚の服装を尋ねたら開示する。 sources: - type: character @@ -275,7 +283,6 @@ evidences: label: 松田の立ち位置の記憶 description: 松田は白い像を見た直後、数歩後ろにいた小野寺から返事を受けており、反射像の位置関係と一致する。 reveal: - mode: conversation condition: 松田に21時35分の目撃時、小野寺がどこから返事をしたか詳しく尋ねたら開示する。 sources: - type: character @@ -286,20 +293,20 @@ evidences: label: 運営法人への予算報告草案 description: 犬塚の端末に、小野寺による正式承認のない予算振替を翌朝報告する草案が残っている。 reveal: - mode: conversation - condition: 小野寺か朝倉に犬塚が翌朝運営法人へ提出予定だった報告について尋ねたら開示する。 + condition: 小野寺か朝倉に犬塚が翌朝運営法人へ提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「運営法人への予算報告草案」に関わる資料を確認したら開示する。 sources: - type: character id: onodera - type: character id: asakura + - type: victim + id: victim supports: [exhibit-budget-found, budget-report-next-day] contradicts: [] - id: skipped-projection-test label: 省略された投影機器テスト description: 松田が事前テストを一部省略していたことが分かるが、犬塚の死とは独立した隠し事である。 reveal: - mode: conversation condition: 松田に点検表の未記入項目を追及したら開示する。 sources: - type: character @@ -310,30 +317,31 @@ evidences: label: 売店在庫の不足記録 description: 朝倉が在庫不足を自費補填して隠していたことが分かるが、投影準備室の事件とは無関係である。 reveal: - mode: conversation condition: 朝倉に売店在庫と帳簿の差について尋ねたら開示する。 sources: - type: character id: asakura supports: [asakura-hid-stock-loss] contradicts: ["lie:asakura-stock-clean"] + - id: death-estimate + label: 投影準備室の死亡推定 + description: 投影準備室の温度と発見時の状態を合わせると、犬塚の死亡は21時24分ごろと見積もられる。21時35分の人影目撃より前である。 + reveal: + condition: 遺体を調べて発見時の状態を確認するか、松田に投影準備室へ入ったときの状態と確認内容を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: matsuda } + supports: [inuzuka-death-2124] + contradicts: [] + revealsDeathTime: true solution: culprit: onodera summary: 犯人は小野寺莉香。21時35分に松田が観察ガラスで見た白い人物像は、投影室内の犬塚ではなかった。投影室は機器点検で暗く、廊下側が明るかったため、ガラスには松田の数歩後ろにいた小野寺が反射していた。小野寺は犬塚と同型の白い展示用上着を着ており、松田は顔を確認せず犬塚だと思い込んだ。小野寺自身はガラスの反射条件を以前から知っていたうえ、松田のすぐ後ろにいたのに「館長はまだ中にいた」と誤認を補強している。犬塚は実際には21時24分ごろ小野寺に襲われていた。翌朝、犬塚は小野寺による無断の予算振替を運営法人へ報告する予定だった。 method: 暗い投影室の観察ガラスに廊下側の自分の姿が反射した目撃を訂正せず、同型の白い上着による誤認を犬塚の生存証明として利用した motive: 無断で行った展示予算の振替を犬塚が翌朝運営法人へ報告する予定で、責任問題になることを恐れたため - requiredFacts: [white-jackets-similar, glass-reflects-dark-booth, onodera-behind-matsuda-2135, figure-was-reflection, onodera-knew-reflection, budget-report-next-day, onodera-killed-inuzuka] secretKeywords: - 犯人は小野寺 - 小野寺が犯人 - 小野寺が犬塚を襲 - 私が犬塚を襲 - 白い人物は小野寺の反射 -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [matsuda-hid-test-failure, asakura-hid-stock-loss] - notes: 目撃証言を「誰を見たか」ではなく「光学的にどちら側の像を見たか」へ分解する。小野寺が反射条件を知っていたことと、実際に松田の後ろにいたことを合わせて故意の利用を確定する。 diff --git a/db/scenarios/storm-research-vessel-missing-launch.yaml b/db/scenarios/storm-research-vessel-missing-launch.yaml index b0d947e..1685113 100644 --- a/db/scenarios/storm-research-vessel-missing-launch.yaml +++ b/db/scenarios/storm-research-vessel-missing-launch.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: storm-research-vessel-missing-launch meta: - title: 調査船みなも、暴風海域の夜 - synopsis: "午後八時四十分、暴風海域で退避航行中の海洋調査船みなもで、主任研究者の瀬尾俊が解析室内で死亡しているのが見つかりました。荒天のため他船との接触はなく、船内にいた関係者は研究員の狩谷琴音、機関担当の水城明、通信担当の野々村岳の三人です。" + title: "調査船みなも号事件" + synopsis: "午後八時四十分、暴風海域で退避航行中の海洋調査船みなもで、主任研究者の瀬尾俊が解析室内で死亡しているのが見つかりました。荒天のため他船と\ + の接触はなく、船内にいた関係者は研究員の狩谷琴音、機関担当の水城明、通信担当の野々村岳の三人です。" category: クローズドサークル difficulty: 4 estimatedMinutes: 15 - tags: [調査船, 暴風, 消失物, 外部犯人] victim: name: 瀬尾俊 introduction: 海洋調査船みなも主任研究者 + foundAt: 20:40 + foundIn: 解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 瀬尾俊は解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「共同研究データの提出履歴」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,7 +49,6 @@ facts: - id: kariya-saw-launch-loss statement: 狩谷琴音は19時42分、小型作業艇が既に失われたことを水城明とともに確認した kind: observation - secret: true - id: launch-note-signed statement: 19時48分の甲板異常メモには、小型作業艇消失の確認者として水城明と狩谷琴音の署名がある kind: physical @@ -54,83 +61,84 @@ facts: - id: kariya-entered-analysis-2014 statement: 20時14分ごろ、狩谷琴音は解析室へ向かった kind: truth - secret: true - id: seo-death-2018 statement: 20時18分ごろ、瀬尾俊は解析室で襲われ死亡した kind: truth - secret: true - id: kariya-killed-seo statement: 狩谷琴音は20時18分ごろ解析室で瀬尾俊を襲い死亡させた kind: truth - secret: true - id: mizuki-saw-kariya-2022 statement: 20時22分ごろ、水城明は解析室側の通路から戻る狩谷琴音を見た kind: observation - id: seo-found-copying statement: 瀬尾俊は狩谷琴音が共同研究データを自分単独の成果として外部提出しようとしていたことに気づいた kind: motive - secret: true - id: seo-report-next-port statement: 瀬尾俊は次の寄港後、狩谷琴音による研究データの不適切な持ち出しを所属機関へ報告する予定だった kind: motive - secret: true - id: mizuki-hid-maintenance-delay statement: 水城明は甲板設備の点検延期を上司に報告していなかった kind: other - secret: true - id: nonomura-private-message statement: 野々村岳は勤務中に業務外の個人的な通信を行っていた kind: other - secret: true - id: body-found-2040 statement: 20時40分、野々村岳が解析室で瀬尾俊の死を発見した kind: observation timeline: - id: launch-loss at: "19:42" - participants: [kariya, mizuki] - facts: [launch-lost-1942, kariya-saw-launch-loss] + participants: [ kariya, mizuki ] + facts: [ launch-lost-1942, kariya-saw-launch-loss ] + record: 消失確認メモ description: 暴風による甲板設備の異常が起き、小型作業艇が既に失われたことを狩谷と水城が確認する。 + location: 甲板 - id: launch-note at: "19:48" - participants: [kariya, mizuki] - facts: [launch-note-signed] + participants: [ kariya, mizuki ] + facts: [ launch-note-signed ] + record: 甲板メモ description: 作業艇消失を含む甲板異常メモに狩谷と水城が署名する。 + location: 甲板 - id: seo-call at: "19:56" - participants: [nonomura] - facts: [seo-alive-1956, launch-gone-before-death] + participants: [ nonomura ] + facts: [ seo-alive-1956, launch-gone-before-death ] description: 野々村が瀬尾と船内通話を行い、作業艇消失後にも瀬尾が生存していたことが確認される。 + location: 船内 - id: kariya-analysis at: "20:14" - participants: [kariya] - facts: [kariya-entered-analysis-2014] + participants: [ kariya ] + facts: [ kariya-entered-analysis-2014 ] description: 狩谷が解析室へ向かう。 + location: 解析室 - id: seo-death at: "20:18" - participants: [kariya] - facts: [seo-death-2018, kariya-killed-seo] + participants: [ kariya ] + facts: [ seo-death-2018, kariya-killed-seo ] description: 狩谷が解析室で瀬尾を襲い、瀬尾は死亡する。 + location: 解析室 - id: corridor-sighting at: "20:22" - participants: [kariya, mizuki] - facts: [mizuki-saw-kariya-2022] + participants: [ kariya, mizuki ] + facts: [ mizuki-saw-kariya-2022 ] description: 水城が解析室側の通路から戻る狩谷を目撃する。 + location: 通路 - id: discovery at: "20:40" - participants: [nonomura, kariya, mizuki] - facts: [body-found-2040] + participants: [ nonomura, kariya, mizuki ] + facts: [ body-found-2040 ] description: 野々村が解析室で瀬尾の死を発見する。 + location: 解析室 characters: - id: kariya name: 狩谷琴音 - role: suspect publicIntroduction: "頭の回転が速く、可能性を広げて話す研究員。" personality: 頭の回転が速く、可能性を広げて話す研究員。作業艇の消失を強調し、船内の三人に限定せず外部犯人の可能性を残そうとする。自分の研究上の不正を瀬尾に見つけられていた。 goals: - 消えた作業艇を外部犯人の逃走手段と思わせたい - 共同研究データを単独成果として提出しようとしていたことを隠したい - knowledge: [kariya-researcher, launch-lost-1942, body-found-2040] + knowledge: [ kariya-researcher, launch-lost-1942, body-found-2040 ] secrets: - fact: kariya-saw-launch-loss disclosure: pressured @@ -153,10 +161,8 @@ characters: strategy: maintain-until-contradicted memories: - id: signed-launch-note - about: launch-note-signed detail: 19時48分、水城と一緒に作業艇消失のメモへ署名したので、事件より前に艇がなかったことを確実に知っている。 - id: next-port-report - about: seo-report-next-port detail: 瀬尾から「次の港に着いたら所属機関へ提出経緯を報告する」と告げられ、研究者として終わると思った。 relationships: - character: mizuki @@ -167,13 +173,20 @@ characters: attitude: 瀬尾の最後の船内通話時刻を正確に覚えているのが厄介 - id: mizuki name: 水城明 - role: witness publicIntroduction: "現場優先の機関担当。" personality: 現場優先の機関担当。設備点検を延期した件を隠したいが、作業艇が失われた時刻と狩谷の署名については記録通り話す。 goals: - 設備点検の延期を隠したい - 作業艇は事件前に失われたと正確に説明したい - knowledge: [mizuki-engineer, launch-lost-1942, kariya-saw-launch-loss, launch-note-signed, mizuki-saw-kariya-2022, body-found-2040] + knowledge: + [ + mizuki-engineer, + launch-lost-1942, + kariya-saw-launch-loss, + launch-note-signed, + mizuki-saw-kariya-2022, + body-found-2040 + ] secrets: - fact: mizuki-hid-maintenance-delay disclosure: pressured @@ -184,10 +197,8 @@ characters: strategy: maintain-until-contradicted memories: - id: launch-gone-with-kariya - about: kariya-saw-launch-loss detail: 作業艇が見当たらないことを狩谷と一緒に確認し、その後二人で異常メモに署名した。 - id: kariya-corridor - about: mizuki-saw-kariya-2022 detail: 20時22分ごろ、解析室側から戻ってくる狩谷と近い距離ですれ違った。 relationships: - character: kariya @@ -195,13 +206,12 @@ characters: attitude: 作業艇の件を知らないふりをする理由が分からず不信感がある - id: nonomura name: 野々村岳 - role: witness publicIntroduction: "調査船の通信担当。" personality: 正確な通信記録を好む担当者。勤務中の私的通信を知られたくない。19時56分に瀬尾本人と話したことには自信がある。 goals: - 業務外の個人的な通信を隠したい - 作業艇消失後も瀬尾が生存していたことを伝えたい - knowledge: [nonomura-radio, seo-alive-1956, launch-gone-before-death, body-found-2040] + knowledge: [ nonomura-radio, seo-alive-1956, launch-gone-before-death, body-found-2040 ] secrets: - fact: nonomura-private-message disclosure: pressured @@ -212,7 +222,6 @@ characters: strategy: maintain-until-contradicted memories: - id: seo-voice-1956 - about: seo-alive-1956 detail: 19時56分に瀬尾と研究データの番号を読み合わせたので、その時点で本人が生きていたことは間違いない。 relationships: - character: kariya @@ -232,14 +241,21 @@ revelations: revealCondition: 水城に作業艇が失われた時刻と、その場にいた確認者、異常メモの署名者を尋ねた。 requires: revelations: [] - evidences: [launch-loss-note] + evidences: [ launch-loss-note ] - type: character id: nonomura revealCondition: 野々村に瀬尾と最後に確実に話した時刻を尋ね、作業艇消失より後だったと確認した。 requires: revelations: [] - evidences: [seo-call-log] - relatedFacts: [launch-lost-1942, kariya-saw-launch-loss, launch-note-signed, seo-alive-1956, launch-gone-before-death] + evidences: [ seo-call-log ] + relatedFacts: + [ + launch-lost-1942, + kariya-saw-launch-loss, + launch-note-signed, + seo-alive-1956, + launch-gone-before-death + ] - id: data-report-motive title: 次の寄港後の不正報告 text: 瀬尾は狩谷が共同研究データを単独成果として提出しようとしていたことを把握し、次の寄港後に所属機関へ報告する予定だった。 @@ -252,97 +268,84 @@ revelations: id: kariya revealCondition: 狩谷に共同研究データの外部提出と瀬尾が次の寄港後に予定していた報告を追及した。 requires: - revelations: [launch-could-not-be-escape] - evidences: [submission-history] - relatedFacts: [seo-found-copying, seo-report-next-port] + revelations: [ launch-could-not-be-escape ] + evidences: [ submission-history ] + relatedFacts: [ seo-found-copying, seo-report-next-port ] evidences: - id: launch-loss-note label: 十九時四十八分の甲板異常メモ description: 小型作業艇が既に失われたことと、確認者として水城と狩谷の署名が残っている。 reveal: - mode: conversation condition: 水城か狩谷に作業艇を最後に確認した時刻と甲板異常メモについて尋ねたら開示する。 sources: - type: character id: mizuki - type: character id: kariya - supports: [launch-lost-1942, kariya-saw-launch-loss, launch-note-signed] - contradicts: ["lie:kariya-outsider-launch"] + supports: [ launch-lost-1942, kariya-saw-launch-loss, launch-note-signed ] + contradicts: [ "lie:kariya-outsider-launch" ] - id: seo-call-log label: 十九時五十六分の船内通話記録 description: 作業艇消失後の19時56分、野々村が瀬尾本人と研究データの確認をしている。 reveal: - mode: conversation condition: 野々村に瀬尾と最後に話した船内通話の内容と時刻を尋ねたら開示する。 sources: - type: character id: nonomura - supports: [seo-alive-1956, launch-gone-before-death] - contradicts: ["lie:kariya-outsider-launch"] + supports: [ seo-alive-1956, launch-gone-before-death ] + contradicts: [ "lie:kariya-outsider-launch" ] - id: analysis-corridor-sighting label: 二十時二十二分の通路目撃 description: 水城は解析室側の通路から戻る狩谷を20時22分ごろに目撃している。 reveal: - mode: conversation condition: 水城に20時15分から25分の間に解析室側で誰を見たか尋ねたら開示する。 sources: - type: character id: mizuki - supports: [mizuki-saw-kariya-2022] - contradicts: ["lie:kariya-no-analysis-room"] + supports: [ mizuki-saw-kariya-2022 ] + contradicts: [ "lie:kariya-no-analysis-room" ] - id: submission-history label: 共同研究データの提出履歴 description: 狩谷が共同研究データを単独成果として外部提出しようとし、瀬尾がその経緯を問題視していたことが分かる。 reveal: - mode: conversation - condition: 狩谷か野々村に瀬尾が直前まで確認していた研究データの提出経緯を尋ねたら開示する。 + condition: 狩谷か野々村に瀬尾が直前まで確認していた研究データの提出経緯を尋ねたら開示する。または遺体・現場を調べ、「共同研究データの提出履歴」に関わる資料を確認したら開示する。 sources: - type: character id: kariya - type: character id: nonomura - supports: [seo-found-copying, seo-report-next-port] + - type: victim + id: victim + supports: [ seo-found-copying, seo-report-next-port ] contradicts: [] - id: delayed-maintenance label: 延期された甲板設備点検 description: 水城が点検延期を報告していなかったことが分かるが、瀬尾の死とは独立した隠し事である。 reveal: - mode: conversation condition: 水城に甲板設備の点検予定と延期について具体的に尋ねたら開示する。 sources: - type: character id: mizuki - supports: [mizuki-hid-maintenance-delay] - contradicts: ["lie:mizuki-maintenance-current"] + supports: [ mizuki-hid-maintenance-delay ] + contradicts: [ "lie:mizuki-maintenance-current" ] - id: private-radio-message label: 野々村の私的通信 description: 野々村が勤務中に個人的な通信をしていたことが分かるが、解析室の事件とは無関係である。 reveal: - mode: conversation condition: 野々村に当直中の業務外通信を追及したら開示する。 sources: - type: character id: nonomura - supports: [nonomura-private-message] - contradicts: ["lie:nonomura-work-only"] + supports: [ nonomura-private-message ] + contradicts: [ "lie:nonomura-work-only" ] solution: culprit: kariya summary: 犯人は狩谷琴音。発見時に小型作業艇がなかったため、事件後に外部犯人が艇で逃げたように見えるが、艇は19時42分に既に失われていた。19時48分の甲板異常メモには狩谷自身の署名があり、狩谷は事件前から作業艇がないことを知っていた。その後19時56分には野々村が瀬尾本人と通話しているため、作業艇消失は瀬尾の死より前である。20時22分には水城が解析室側から戻る狩谷を目撃している。瀬尾は狩谷による共同研究データの不適切な外部提出を次の寄港後に報告する予定だった。狩谷が知っているはずの時系列を隠して外部犯人説を強調したことが重要な矛盾となる。 method: 事件前から失われていた作業艇を事件後の逃走手段だったように扱い、船内三人以外の犯人が存在する可能性を作ったうえで、自分の解析室への出入りを隠した motive: 共同研究データを単独成果として外部提出しようとした件を、瀬尾が次の寄港後に所属機関へ報告する予定だったため - requiredFacts: [launch-lost-1942, kariya-saw-launch-loss, launch-note-signed, seo-alive-1956, launch-gone-before-death, mizuki-saw-kariya-2022, seo-report-next-port, kariya-killed-seo] secretKeywords: - 犯人は狩谷 - 狩谷が犯人 - 狩谷が瀬尾を襲 - 私が瀬尾を襲 - 作業艇で外部犯人を偽装 -quality: - expectedQuestionCount: - min: 11 - max: 22 - requiredEvidence: - min: 3 - redHerrings: [mizuki-hid-maintenance-delay, nonomura-private-message] - notes: 漂流船の歴史的ミステリに見られる「欠けた小舟が人の移動を意味する」という連想を反転。消失物の時刻を確定し、容疑者がその時刻を知っていた点まで追う。 diff --git a/db/scenarios/storm-seminar-echoed-testimony.yaml b/db/scenarios/storm-seminar-echoed-testimony.yaml index 9417013..9da83ff 100644 --- a/db/scenarios/storm-seminar-echoed-testimony.yaml +++ b/db/scenarios/storm-seminar-echoed-testimony.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: storm-seminar-echoed-testimony meta: - title: 山中研究会館、嵐の夜 + title: "山中研究会殺人録" synopsis: "午後九時四十五分、嵐で道路が閉鎖された山中の研究会館で、主催者・鷺沢修が西側の事務室で死亡しているのが見つかりました。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [研究会, 嵐, 証言, 情報源] victim: name: 鷺沢修 introduction: 山中の研究会館の研究会主催者 + foundAt: 21:45 + foundIn: 西側事務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 鷺沢修は西側事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「単独名義の投稿原稿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -38,7 +45,6 @@ facts: - id: kuga-claimed-saw-east statement: 久我真帆は21時18分ごろ鷺沢修が東資料室へ向かうのを見たと主張した kind: testimony - secret: true - id: naruse-learned-from-kuga statement: 成瀬灯が鷺沢修は東資料室にいると思った根拠は、久我真帆からそう聞いたことだった kind: testimony @@ -57,83 +63,95 @@ facts: - id: kuga-stole-results statement: 久我真帆は鷺沢修の未発表分析結果を自分単独の成果として先に投稿しようとしていた kind: motive - secret: true - id: sagisawa-found-submission statement: 鷺沢修は事件当日の夜、久我真帆が未発表結果を無断で投稿準備していることを知った kind: motive - secret: true - id: sagisawa-confronted-kuga statement: 20時58分ごろ、鷺沢修は久我真帆に投稿を取り下げるよう求め、応じなければ共同研究から外すと告げた kind: motive - secret: true - id: kuga-killed-sagisawa statement: 21時10分ごろ、久我真帆は西側事務室で鷺沢修を襲い死亡させた kind: truth - secret: true - id: kuga-started-rumor statement: 21時18分ごろ、久我真帆は成瀬灯に鷺沢修が東資料室へ行ったと嘘を伝えた kind: truth - secret: true - id: naruse-repeated-rumor statement: 21時22分ごろ、成瀬灯は白石慧に鷺沢修が東資料室にいると伝えた kind: observation - id: naruse-secret-embargo statement: 成瀬灯は公開前の論文集の内容を知人へ漏らしていた kind: other - secret: true - id: shiraishi-secret-data statement: 白石慧は許可なく研究会の参加者データを自分の調査に流用していた kind: other - secret: true - id: body-found-2145 statement: 21時45分、成瀬灯が西側事務室で鷺沢修の死を発見した kind: observation timeline: - id: submission-confrontation at: "20:58" - participants: [kuga] - facts: [kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga] + participants: [ kuga ] + facts: [ kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga ] description: 鷺沢が久我の無断投稿準備を知り、取り下げと共同研究からの除外を告げる。 + location: 会館内 - id: sagisawa-death at: "21:10" - participants: [kuga] - facts: [kuga-killed-sagisawa] + participants: [ kuga ] + facts: [ kuga-killed-sagisawa ] description: 久我が西側事務室で鷺沢を襲う。 + location: 西側事務室 - id: west-corridor-sighting at: "21:16" - participants: [kuga, shiraishi] - facts: [shiraishi-saw-kuga-west] + participants: [ kuga, shiraishi ] + facts: [ shiraishi-saw-kuga-west ] description: 白石が西側事務室へ続く廊下から出てくる久我を見かける。 + location: 西廊下 - id: rumor-starts at: "21:18" - participants: [kuga, naruse] - facts: [kuga-claimed-saw-east, kuga-started-rumor, no-independent-east-sighting] + participants: [ kuga, naruse ] + facts: [ kuga-claimed-saw-east, kuga-started-rumor, no-independent-east-sighting ] description: 久我が成瀬に「鷺沢は東資料室へ行った」と伝え、存在しない目撃情報の起点を作る。 + location: 会館内 - id: rumor-repeated at: "21:22" - participants: [naruse, shiraishi] - facts: [naruse-learned-from-kuga, naruse-repeated-rumor, shiraishi-learned-from-naruse] + participants: [ naruse, shiraishi ] + facts: + [ + naruse-learned-from-kuga, + naruse-repeated-rumor, + shiraishi-learned-from-naruse + ] description: 成瀬が白石へ同じ話を伝え、一つの情報が複数人の証言へ増殖する。 + location: 会館内 - id: east-room-still-unused at: "21:40" - participants: [shiraishi] - facts: [east-room-unused] + participants: [ shiraishi ] + facts: [ east-room-unused ] + record: 閲覧卓の位置 description: 東資料室は片付け時のままで、鷺沢が資料を使った痕跡がない。 + location: 東資料室 - id: discovery at: "21:45" - participants: [naruse, kuga, shiraishi] - facts: [body-found-2145] + participants: [ naruse, kuga, shiraishi ] + facts: [ body-found-2145 ] description: 成瀬が西側事務室で鷺沢の死を発見する。 + location: 西側事務室 characters: - id: kuga name: 久我真帆 - role: suspect publicIntroduction: "自信が強く議論の組み立てが速い研究員。" personality: 自信が強く議論の組み立てが速い研究員。自分の証言だけでなく「成瀬も白石も同じ認識だった」と一致の数を強調する。未発表成果の扱いを問われると攻撃的になる。 goals: - 未発表成果を無断投稿しようとしたことを隠したい - 東資料室の話を複数人の独立証言に見せたい - knowledge: [kuga-researcher, kuga-claimed-saw-east, naruse-learned-from-kuga, shiraishi-learned-from-naruse, body-found-2145] + knowledge: + [ + kuga-researcher, + kuga-claimed-saw-east, + naruse-learned-from-kuga, + shiraishi-learned-from-naruse, + body-found-2145 + ] secrets: - fact: kuga-stole-results disclosure: pressured @@ -156,7 +174,6 @@ characters: strategy: maintain-until-contradicted memories: - id: repeated-story - about: naruse-repeated-rumor detail: 自分が成瀬へ一度言えば、研究会の連絡役である成瀬から他の参加者へ自然に話が広がると分かっていた。 relationships: - character: naruse @@ -167,13 +184,18 @@ characters: attitude: 西廊下で見られたことが気になる - id: naruse name: 成瀬灯 - role: witness publicIntroduction: "情報整理が得意な編集者。" personality: 情報整理が得意な編集者。ただし、誰から聞いた情報かを省いて「分かっていること」として話す癖がある。公開前情報の漏洩を隠している。 goals: - 論文集の内容を漏らしたことを隠したい - 東資料室の話は自分の目撃ではないと明確にしたい - knowledge: [naruse-editor, naruse-learned-from-kuga, naruse-repeated-rumor, body-found-2145] + knowledge: + [ + naruse-editor, + naruse-learned-from-kuga, + naruse-repeated-rumor, + body-found-2145 + ] secrets: - fact: naruse-secret-embargo disclosure: pressured @@ -184,7 +206,6 @@ characters: strategy: maintain-until-contradicted memories: - id: heard-from-kuga - about: naruse-learned-from-kuga detail: 久我から「鷺沢さん、東の資料室へ行ったよ」と聞き、それを確認済みの情報だと思って白石にも伝えた。 relationships: - character: kuga @@ -195,13 +216,19 @@ characters: attitude: 自分から東資料室の話を伝えた相手 - id: shiraishi name: 白石慧 - role: witness publicIntroduction: "研究会に参加していた大学院生。" personality: 慎重な大学院生で、人の発言を時刻と一緒に覚えている。参加者データを無断利用した秘密はあるが、21時台の廊下で見た久我については具体的に話せる。 goals: - 参加者データの無断流用を隠したい - 久我を西側で見た事実と、東資料室の話を成瀬から聞いただけだという点を伝えたい - knowledge: [shiraishi-student, shiraishi-saw-kuga-west, shiraishi-learned-from-naruse, east-room-unused, body-found-2145] + knowledge: + [ + shiraishi-student, + shiraishi-saw-kuga-west, + shiraishi-learned-from-naruse, + east-room-unused, + body-found-2145 + ] secrets: - fact: shiraishi-secret-data disclosure: pressured @@ -212,7 +239,6 @@ characters: strategy: maintain-until-contradicted memories: - id: west-kuga - about: shiraishi-saw-kuga-west detail: 九時十六分ごろ、西側の廊下から久我が出てきた。二分ほど後に成瀬から「鷺沢は東にいるらしい」と聞き、変だとは思わなかった。 relationships: - character: kuga @@ -235,14 +261,20 @@ revelations: revealCondition: 成瀬に東資料室の情報を自分で見たのか、誰から聞いたのか順に尋ね、久我が情報源だと確認したら開示する。 requires: revelations: [] - evidences: [source-chain] + evidences: [ source-chain ] - type: character id: shiraishi revealCondition: 白石に東資料室の話を誰から聞いたか尋ね、成瀬経由だったと確認したら開示する。 requires: revelations: [] - evidences: [source-chain] - relatedFacts: [kuga-claimed-saw-east, naruse-learned-from-kuga, shiraishi-learned-from-naruse, no-independent-east-sighting] + evidences: [ source-chain ] + relatedFacts: + [ + kuga-claimed-saw-east, + naruse-learned-from-kuga, + shiraishi-learned-from-naruse, + no-independent-east-sighting + ] - id: east-sighting-collapses title: 東資料室の目撃は成立しない text: 東資料室は片付け時のままで、久我の主張以外に鷺沢が使った痕跡がない。一方、白石は久我を事件直後の西側廊下で見ている。 @@ -255,15 +287,15 @@ revelations: id: shiraishi revealCondition: 白石に西廊下で久我を見た時刻と東資料室の状態を合わせて尋ねたら開示する。 requires: - revelations: [testimonies-share-one-source] - evidences: [west-corridor, unused-east-room] + revelations: [ testimonies-share-one-source ] + evidences: [ west-corridor, unused-east-room ] - type: character id: kuga revealCondition: 久我に東資料室の具体的な目撃状況を追及し、他の裏付けがないことを示したら開示する。 requires: - revelations: [testimonies-share-one-source] - evidences: [unused-east-room] - relatedFacts: [shiraishi-saw-kuga-west, east-room-unused, kuga-started-rumor] + revelations: [ testimonies-share-one-source ] + evidences: [ unused-east-room ] + relatedFacts: [ shiraishi-saw-kuga-west, east-room-unused, kuga-started-rumor ] - id: stolen-results-motive title: 未発表成果の無断投稿 text: 鷺沢は久我が共同研究の未発表結果を単独成果として投稿しようとしていることを知り、取り下げなければ共同研究から外すと告げていた。 @@ -276,104 +308,96 @@ revelations: id: kuga revealCondition: 久我に投稿原稿と鷺沢からの取り下げ要求を具体的に示したら開示する。 requires: - revelations: [east-sighting-collapses] - evidences: [draft-submission] + revelations: [ east-sighting-collapses ] + evidences: [ draft-submission ] - type: character id: naruse revealCondition: 成瀬に久我の投稿原稿と鷺沢が問題視していた権利関係を尋ねたら開示する。 requires: - revelations: [east-sighting-collapses] - evidences: [draft-submission] - relatedFacts: [kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga] + revelations: [ east-sighting-collapses ] + evidences: [ draft-submission ] + relatedFacts: [ kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga ] evidences: - id: source-chain label: 東資料室情報の伝達経路 description: 成瀬の情報源は久我、白石の情報源は成瀬であり、独立した三つの目撃ではない。 reveal: - mode: conversation condition: 成瀬と白石の双方に「自分で見たのか、誰から聞いたのか」を尋ねたら開示する。 sources: - type: character id: naruse - type: character id: shiraishi - supports: [naruse-learned-from-kuga, shiraishi-learned-from-naruse, no-independent-east-sighting] - contradicts: ["lie:kuga-east-sighting"] + supports: + [ + naruse-learned-from-kuga, + shiraishi-learned-from-naruse, + no-independent-east-sighting + ] + contradicts: [ "lie:kuga-east-sighting" ] - id: west-corridor label: 二十一時十六分の西廊下目撃 description: 白石は事件直後の時刻に、西側事務室へ続く廊下から出てくる久我を見ている。 reveal: - mode: conversation condition: 白石に21時10分から20分ごろ西側廊下で誰を見たか尋ねたら開示する。 sources: - type: character id: shiraishi - supports: [shiraishi-saw-kuga-west] - contradicts: ["lie:kuga-east-sighting"] + supports: [ shiraishi-saw-kuga-west ] + contradicts: [ "lie:kuga-east-sighting" ] - id: unused-east-room label: 手つかずの東資料室 description: 東資料室は21時前の片付け状態から机も資料箱も動いておらず、鷺沢がそこで資料を使った形跡がない。 reveal: - mode: conversation condition: 白石に東資料室を最後に確認した時の状態を尋ねるか、久我に鷺沢が何をしに行ったのか具体的に質問したら開示する。 sources: - type: character id: shiraishi - type: character id: kuga - supports: [east-room-unused] - contradicts: ["lie:kuga-east-sighting"] + supports: [ east-room-unused ] + contradicts: [ "lie:kuga-east-sighting" ] - id: draft-submission label: 単独名義の投稿原稿 description: 久我の端末には共同研究の未発表結果を自分単独の成果として投稿する準備が残り、鷺沢からの取り下げ要求も確認できる。 reveal: - mode: conversation - condition: 久我か成瀬に事件直前の投稿原稿と鷺沢との対立について尋ねたら開示する。 + condition: 久我か成瀬に事件直前の投稿原稿と鷺沢との対立について尋ねたら開示する。または遺体・現場を調べ、「単独名義の投稿原稿」に関わる資料を確認したら開示する。 sources: - type: character id: kuga - type: character id: naruse - supports: [kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga] - contradicts: ["lie:kuga-no-stolen-results"] + - type: victim + id: victim + supports: [ kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga ] + contradicts: [ "lie:kuga-no-stolen-results" ] - id: embargo-leak label: 公開前論文集の漏洩 description: 成瀬が公開前の論文集内容を知人へ送っていた記録。東資料室の目撃連鎖とは独立した秘密である。 reveal: - mode: conversation condition: 成瀬に公開前資料の共有先を尋ね、漏洩を否定したら開示する。 sources: - type: character id: naruse - supports: [naruse-secret-embargo] - contradicts: ["lie:naruse-no-leak"] + supports: [ naruse-secret-embargo ] + contradicts: [ "lie:naruse-no-leak" ] - id: participant-data-use label: 参加者データの無断流用 description: 白石が研究会の参加者情報を自分の調査に転用していたことが分かるが、主事件とは関係しない。 reveal: - mode: conversation condition: 白石に参加者データの利用目的を尋ね、無断利用を否定したら開示する。 sources: - type: character id: shiraishi - supports: [shiraishi-secret-data] - contradicts: ["lie:shiraishi-no-data-use"] + supports: [ shiraishi-secret-data ] + contradicts: [ "lie:shiraishi-no-data-use" ] solution: culprit: kuga summary: 犯人は久我真帆。共同研究の未発表成果を単独名義で投稿しようとしたことを鷺沢に知られ、取り下げなければ共同研究から外すと告げられた。久我は西側事務室で鷺沢を襲った後、成瀬へ「鷺沢は東資料室へ行った」と嘘を伝えた。成瀬はそれを白石へ伝え、後の聞き取りでは三人とも東資料室という同じ結論を口にしたため、複数の目撃証言があるように見えた。しかし直接の目撃を主張する情報源は久我一人だけだった。東資料室は手つかずで、白石は事件直後に西側廊下から出てくる久我を見ている。証言の一致は、同じ嘘が伝言された結果だった。 method: 鷺沢を襲った後、自分を起点に「鷺沢は東資料室にいる」という偽情報を流し、伝言で増えた一致証言を犯行時刻と場所の裏付けに見せかけた motive: 共同研究の未発表成果を無断で単独投稿しようとしたことが発覚し、投稿撤回と研究からの除外を恐れたため - requiredFacts: [kuga-stole-results, sagisawa-confronted-kuga, kuga-killed-sagisawa, shiraishi-saw-kuga-west, kuga-started-rumor, naruse-learned-from-kuga, shiraishi-learned-from-naruse, no-independent-east-sighting, east-room-unused] secretKeywords: - 犯人は久我 - 久我が鷺沢を襲 - 久我が東資料室の嘘を流 - 三人の証言は同じ情報源 -quality: - expectedQuestionCount: - min: 12 - max: 26 - requiredEvidence: - min: 3 - redHerrings: [naruse-secret-embargo, shiraishi-secret-data] - notes: NPC会話との相性を重視した情報源追跡型。証言内容ではなく「誰から知ったか」を掘らないと、一つの嘘が三票に見える。 diff --git a/db/scenarios/thunder-cablecar-occupied-cabin.yaml b/db/scenarios/thunder-cablecar-occupied-cabin.yaml index 267ffc1..b2230f7 100644 --- a/db/scenarios/thunder-cablecar-occupied-cabin.yaml +++ b/db/scenarios/thunder-cablecar-occupied-cabin.yaml @@ -1,15 +1,34 @@ schemaVersion: 1 id: thunder-cablecar-occupied-cabin meta: - title: 霧岳山頂駅、落雷の夜 - synopsis: "午後九時三十八分、霧岳ロープウェイ山頂駅の運行事務室で、駅長の高瀬修司が死亡しているのが見つかりました。落雷で本線索道は停止し、山道も土砂流入で通行できません。午後八時五十分以降、山頂駅へ出入りした者はいません。" + title: "霧岳ロープウェイ殺人事件" + synopsis: "午後九時三十八分、霧岳ロープウェイ山頂駅の運行事務室で、駅長の高瀬修司が死亡しているのが見つかりました。落雷で本線索道は停止し、山道も土\ + 砂流入で通行できません。午後八時五十分以降、山頂駅へ出入りした者はいません。" category: クローズドサークル difficulty: 4 estimatedMinutes: 15 - tags: [山頂駅, 雷雨, 重量センサー, アリバイ] victim: name: 高瀬修司 introduction: 霧岳ロープウェイ山頂駅駅長 + foundAt: 21:38 + foundIn: 運行事務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 高瀬修司は運行事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「制動点検記録と作業履歴の不一致」に関わる資料が残されている。 +places: + - id: emergency-cabin + name: 非常用搬器 + shortName: 非常搬器 + introduction: 非常時と保守点検に使う、小型の予備搬器 + situation: 営業終了後の点検位置で停止している + findings: + - id: seat-senses-weight + statement: 点検席のセンサーは人物を識別せず、一定以上の重量が掛かると「乗員1」を表示する。 + - id: tool-case-triggers-seat + statement: 保守用工具ケースだけを点検席へ置いても、監視盤の表示は「乗員1」へ切り替わる。 briefing: |- ——事件の記録を読み上げます。 @@ -41,15 +60,12 @@ facts: - id: sudo-falsified-brake-tests statement: 須藤拓海は期限内に実施していない一部の制動点検を実施済みとして記録していた kind: motive - secret: true - id: takase-found-false-tests statement: 高瀬修司は事件当日、須藤拓海の制動点検記録と実作業が一致しないことに気づいた kind: motive - secret: true - id: takase-planned-suspension statement: 高瀬修司は翌朝、須藤拓海を運転業務から外し、安全管理部へ記録不備を報告する予定だった kind: motive - secret: true - id: cabin-display-weight-based statement: 非常用搬器の『乗員1』表示は人を識別せず、座席下の重量センサーが一定以上の荷重を検知すると点灯する kind: physical @@ -59,37 +75,30 @@ facts: - id: sudo-left-tool-case statement: 21時09分ごろ、須藤拓海は保守用工具ケースを非常用搬器の点検席へ置いた kind: truth - secret: true - id: occupancy-display-on statement: 21時10分から21時30分まで、非常用搬器の監視盤には『乗員1』が連続表示された kind: physical - id: sudo-left-cabin-area statement: 21時12分ごろ、須藤拓海は非常用搬器前を離れた kind: truth - secret: true - id: enomoto-saw-sudo-2118 statement: 21時18分ごろ、榎本澄は運行事務室へ続く職員通路で須藤拓海を見た kind: observation - id: sudo-killed-takase statement: 21時22分ごろ、須藤拓海は運行事務室で高瀬修司を襲い死亡させた kind: truth - secret: true - id: sudo-returned-cabin statement: 21時28分ごろ、須藤拓海は非常用搬器前へ戻った kind: truth - secret: true - id: enomoto-kept-lost-wallet statement: 榎本澄は売店で拾った財布を正式な遺失物処理に回さず、自分のロッカーへ入れていた kind: other - secret: true - id: nagamine-bypassed-heater statement: 長峰礼は規定外の方法で融雪ヒーターの警報を一時的に無効化していた kind: other - secret: true - id: orihara-entered-restricted-deck statement: 折原壮は撮影のため立入禁止の保守デッキへ無断で出ていた kind: other - secret: true - id: body-found-2138 statement: 21時38分、長峰礼が運行事務室で高瀬修司の死を発見した kind: observation @@ -97,55 +106,72 @@ facts: timeline: - id: tool-case-placed at: "21:09" - participants: [sudo] - facts: [cabin-display-weight-based, tool-case-heavy-enough, sudo-left-tool-case] + participants: [ sudo ] + facts: [ cabin-display-weight-based, tool-case-heavy-enough, sudo-left-tool-case ] + record: 工具ケース description: 須藤が保守用工具ケースを非常用搬器の点検席へ置く。 + location: 非常搬器 - id: occupancy-start at: "21:10" participants: [] - facts: [occupancy-display-on] + facts: [ occupancy-display-on ] + record: 乗員表示 description: 工具ケースの荷重で監視盤に『乗員1』が表示される。 + location: 山頂駅 - id: sudo-leaves-cabin at: "21:12" - participants: [sudo] - facts: [sudo-left-cabin-area] + participants: [ sudo ] + facts: [ sudo-left-cabin-area ] description: 須藤が非常用搬器前を離れる。 + location: 非常搬器 - id: enomoto-sighting at: "21:18" - participants: [sudo, enomoto] - facts: [enomoto-saw-sudo-2118] + participants: [ sudo, enomoto ] + facts: [ enomoto-saw-sudo-2118 ] description: 榎本が職員通路で須藤を目撃する。 + location: 職員通路 - id: takase-death at: "21:22" - participants: [sudo] - facts: [sudo-killed-takase] + participants: [ sudo ] + facts: [ sudo-killed-takase ] description: 須藤が運行事務室で高瀬を襲い、高瀬は死亡する。 + location: 運行事務室 - id: sudo-return at: "21:28" - participants: [sudo] - facts: [sudo-returned-cabin] + participants: [ sudo ] + facts: [ sudo-returned-cabin ] description: 須藤が非常用搬器前へ戻る。 + location: 非常搬器 - id: occupancy-end at: "21:30" - participants: [sudo] - facts: [occupancy-display-on] + participants: [ sudo ] + facts: [ occupancy-display-on ] + record: 乗員表示 description: 須藤が工具ケースを点検席から下ろし、『乗員1』表示が消える。 + location: 非常搬器 - id: discovery at: "21:38" - participants: [nagamine, sudo, enomoto, orihara] - facts: [body-found-2138] + participants: [ nagamine, sudo, enomoto, orihara ] + facts: [ body-found-2138 ] description: 長峰が運行事務室で高瀬の死を発見する。 + location: 運行事務室 characters: - id: sudo name: 須藤拓海 - role: suspect publicIntroduction: "ロープウェイの運転主任。" personality: 安全規程を熟知し、自信を持って機器表示を説明する運転主任。自分の点検記録の不備を隠し、監視盤の『乗員1』を人物の在席記録であるかのように語る。 goals: - 制動点検記録の虚偽を隠したい - 二十分続いた『乗員1』表示を、自分が点検席に座っていた証拠として通したい - knowledge: [sudo-operator, cabin-display-weight-based, tool-case-heavy-enough, occupancy-display-on, body-found-2138] + knowledge: + [ + sudo-operator, + cabin-display-weight-based, + tool-case-heavy-enough, + occupancy-display-on, + body-found-2138 + ] secrets: - fact: sudo-falsified-brake-tests disclosure: pressured @@ -172,7 +198,6 @@ characters: strategy: maintain-until-contradicted memories: - id: suspension-threat - about: takase-planned-suspension detail: 高瀬から「明朝から運転を外す。安全管理部にも出す」と言われ、資格まで失うのではないかと焦った。 relationships: - character: nagamine @@ -183,13 +208,18 @@ characters: attitude: 通路ですれ違ったことを覚えていそうで警戒している - id: enomoto name: 榎本澄 - role: witness publicIntroduction: "丁寧で記憶力のよい案内係。" personality: 丁寧で記憶力のよい案内係。拾った財布を正式処理しなかったことを隠したいが、21時18分に職員通路で須藤とすれ違ったことは明確に覚えている。 goals: - 拾得物を自分のロッカーへ入れたことを隠したい - 21時18分の須藤の目撃は正確に話したい - knowledge: [enomoto-guide, enomoto-saw-sudo-2118, occupancy-display-on, body-found-2138] + knowledge: + [ + enomoto-guide, + enomoto-saw-sudo-2118, + occupancy-display-on, + body-found-2138 + ] secrets: - fact: enomoto-kept-lost-wallet disclosure: pressured @@ -200,18 +230,23 @@ characters: strategy: maintain-until-contradicted memories: - id: corridor-sudo - about: enomoto-saw-sudo-2118 detail: 21時18分ごろ、搬器にいるはずの須藤が運行事務室側から歩いてきたので、監視盤の表示を見間違えたのかと思った。 relationships: [] - id: nagamine name: 長峰礼 - role: witness publicIntroduction: "機械の仕様を優先して考える設備担当。" personality: 機械の仕様を優先して考える設備担当。融雪設備の手順違反を隠したいが、非常用搬器の乗員表示が単なる荷重判定だと知っている。 goals: - 融雪ヒーターの警報を無効化したことを隠したい - 『乗員1』表示の意味は技術的に正確に説明する - knowledge: [nagamine-maintenance, cabin-display-weight-based, tool-case-heavy-enough, occupancy-display-on, body-found-2138] + knowledge: + [ + nagamine-maintenance, + cabin-display-weight-based, + tool-case-heavy-enough, + occupancy-display-on, + body-found-2138 + ] secrets: - fact: nagamine-bypassed-heater disclosure: pressured @@ -222,7 +257,6 @@ characters: strategy: maintain-until-contradicted memories: - id: occupancy-test - about: tool-case-heavy-enough detail: 保守点検では工具ケースを席へ置いただけで『乗員1』になることがあるので、表示を人員確認には使わないよう教えている。 relationships: - character: sudo @@ -230,13 +264,12 @@ characters: attitude: センサーの限界を知っているはずなのに表示を人の証明として強調するのが不自然だと思う - id: orihara name: 折原壮 - role: suspect publicIntroduction: "山頂駅にいた山岳写真家。" personality: 好奇心が強く規則を軽視しがちな山岳写真家。立入禁止の保守デッキへ出たことを隠すため行動をぼかすが、職員同士の事情には詳しくない。 goals: - 保守デッキへ無断で出たことを隠したい - 駅職員の内部事情に巻き込まれたくない - knowledge: [orihara-photographer, body-found-2138] + knowledge: [ orihara-photographer, body-found-2138 ] secrets: - fact: orihara-entered-restricted-deck disclosure: pressured @@ -247,7 +280,6 @@ characters: strategy: maintain-until-contradicted memories: - id: heavy-case - about: sudo-left-tool-case detail: 21時すぎ、須藤が大きな工具ケースを非常用搬器へ運び込むところを遠目に見た。 relationships: [] @@ -263,14 +295,20 @@ revelations: revealCondition: 長峰に『乗員1』表示が何を検知しているのか尋ね、工具ケースでも反応することを確認した。 requires: revelations: [] - evidences: [seat-weight-spec] + evidences: [ seat-weight-spec ] - type: character id: orihara revealCondition: 折原に21時すぎ非常用搬器付近で見た物を尋ね、須藤が重い工具ケースを運び込んだことを確認した。 requires: revelations: [] - evidences: [seat-weight-spec] - relatedFacts: [cabin-display-weight-based, tool-case-heavy-enough, sudo-left-tool-case, occupancy-display-on] + evidences: [ seat-weight-spec ] + relatedFacts: + [ + cabin-display-weight-based, + tool-case-heavy-enough, + sudo-left-tool-case, + occupancy-display-on + ] - id: sudo-corridor-contradiction title: 搬器にいるはずの須藤 text: 21時18分ごろ、榎本は運行事務室へ続く職員通路で須藤を目撃しており、搬器の点検席に居続けたという説明と両立しない。 @@ -281,9 +319,9 @@ revelations: id: enomoto revealCondition: 榎本に21時台の職員通路で会った人物を尋ね、須藤の目撃時刻を具体化した。 requires: - revelations: [occupancy-is-weight] - evidences: [staff-corridor-sighting] - relatedFacts: [sudo-left-cabin-area, enomoto-saw-sudo-2118] + revelations: [ occupancy-is-weight ] + evidences: [ staff-corridor-sighting ] + relatedFacts: [ sudo-left-cabin-area, enomoto-saw-sudo-2118 ] - id: sudo-safety-motive title: 翌朝に外される運転業務 text: 高瀬は須藤の制動点検記録の虚偽を発見し、翌朝から須藤を運転業務から外して安全管理部へ報告する予定だった。 @@ -294,98 +332,101 @@ revelations: id: sudo revealCondition: 須藤に高瀬が照合していた制動点検記録と翌朝の処分を追及し、運転業務を外される恐れを明確にした。 requires: - revelations: [sudo-corridor-contradiction] - evidences: [brake-test-diff] + revelations: [ sudo-corridor-contradiction ] + evidences: [ brake-test-diff ] - type: character id: nagamine revealCondition: 長峰に高瀬が確認していた制動点検の実作業を尋ね、記録との不一致へつなげた。 requires: revelations: [] - evidences: [brake-test-diff] - relatedFacts: [sudo-falsified-brake-tests, takase-found-false-tests, takase-planned-suspension] + evidences: [ brake-test-diff ] + relatedFacts: + [ + sudo-falsified-brake-tests, + takase-found-false-tests, + takase-planned-suspension + ] evidences: - id: seat-weight-spec label: 非常用搬器の座席重量仕様 description: 『乗員1』は一定以上の荷重で点灯し、保守用工具ケースだけでも同じ表示になる。21時台にはそのケースが点検席へ置かれていた。 reveal: - mode: conversation - condition: 須藤、長峰、折原のいずれかに『乗員1』表示の検知方式と工具ケースについて尋ねたら開示する。 + condition: 須藤、長峰、折原のいずれかに『乗員1』表示の検知方式と工具ケースについて尋ねたら開示する。または非常用搬器を調べ、座席センサーが重量だけを検知することと工具ケースの重さを確認したら開示する。 sources: - { type: character, id: sudo } - { type: character, id: nagamine } - { type: character, id: orihara } - supports: [cabin-display-weight-based, tool-case-heavy-enough, sudo-left-tool-case, occupancy-display-on] - contradicts: ["lie:sudo-cabin-alibi", "lie:sudo-no-toolcase-seat"] + - { type: location, id: emergency-cabin } + supports: + [ + cabin-display-weight-based, + tool-case-heavy-enough, + sudo-left-tool-case, + occupancy-display-on + ] + contradicts: [ "lie:sudo-cabin-alibi", "lie:sudo-no-toolcase-seat" ] - id: staff-corridor-sighting label: 二十一時十八分の職員通路目撃 description: 榎本は21時18分ごろ、運行事務室へ続く職員通路で須藤とすれ違っている。 reveal: - mode: conversation condition: 榎本に21時15分から20分ごろ職員通路で誰を見たか尋ねたら開示する。 sources: - { type: character, id: enomoto } - supports: [enomoto-saw-sudo-2118] - contradicts: ["lie:sudo-cabin-alibi"] + supports: [ enomoto-saw-sudo-2118 ] + contradicts: [ "lie:sudo-cabin-alibi" ] - id: brake-test-diff label: 制動点検記録と作業履歴の不一致 description: 須藤が実施済みと記した点検の一部に作業履歴がなく、高瀬が翌朝の運転停止と安全管理部への報告を記している。 reveal: - mode: conversation - condition: 須藤か長峰に高瀬が事件直前に確認していた制動点検について尋ね、記録と実作業の不一致を追及したら開示する。 + condition: 須藤か長峰に高瀬が事件直前に確認していた制動点検について尋ね、記録と実作業の不一致を追及したら開示する。または遺体・現場を調べ、「制動点検記録と作業履歴の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: sudo } - { type: character, id: nagamine } - supports: [sudo-falsified-brake-tests, takase-found-false-tests, takase-planned-suspension] + - { type: victim, id: victim } + supports: + [ + sudo-falsified-brake-tests, + takase-found-false-tests, + takase-planned-suspension + ] contradicts: [] - id: lost-wallet label: 榎本のロッカーにある拾得財布 description: 売店で拾われた財布が正式な遺失物処理をされず榎本のロッカーに入っていたが、事件とは無関係だった。 reveal: - mode: conversation condition: 榎本に拾得物を私物ロッカーへ入れていないか尋ね、否定を続けたら開示する。 sources: - { type: character, id: enomoto } - supports: [enomoto-kept-lost-wallet] - contradicts: ["lie:enomoto-no-wallet"] + supports: [ enomoto-kept-lost-wallet ] + contradicts: [ "lie:enomoto-no-wallet" ] - id: heater-bypass-log label: 融雪ヒーターの警報停止履歴 description: 長峰が規定外に警報を一時停止していたことが分かるが、運行事務室の事件とは別件である。 reveal: - mode: conversation condition: 長峰に融雪ヒーターの警報を止めていないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: nagamine } - supports: [nagamine-bypassed-heater] - contradicts: ["lie:nagamine-no-bypass"] + supports: [ nagamine-bypassed-heater ] + contradicts: [ "lie:nagamine-no-bypass" ] - id: restricted-deck-photo label: 保守デッキで撮られた写真 description: 折原が立入禁止の保守デッキへ出て撮影していたことが分かるが、事件時刻の運行事務室とは結びつかない。 reveal: - mode: conversation condition: 折原に保守デッキへ出ていないか尋ね、撮影データの場所を検証したら開示する。 sources: - { type: character, id: orihara } - supports: [orihara-entered-restricted-deck] - contradicts: ["lie:orihara-no-restricted-deck"] + supports: [ orihara-entered-restricted-deck ] + contradicts: [ "lie:orihara-no-restricted-deck" ] solution: culprit: sudo summary: 犯人は須藤拓海。実施していない制動点検を実施済みとして記録していたことを高瀬に見抜かれ、翌朝から運転業務を外され安全管理部へ報告される予定だった。須藤は21時09分ごろ、非常用搬器の点検席へ重い工具ケースを置いた。座席下のセンサーは人物を識別せず一定以上の荷重だけで『乗員1』を表示するため、監視盤には21時10分から30分まで一人が乗っているように見えた。須藤本人は21時12分ごろ搬器前を離れ、21時18分には榎本が職員通路で目撃している。21時22分ごろ高瀬を襲った後、21時28分ごろ搬器前へ戻った。 method: 重い工具ケースで非常用搬器の重量センサーを反応させ、『乗員1』表示を在席証明に見せかけて運行事務室へ移動した motive: 制動点検記録の虚偽が発覚し、翌朝から運転業務を外され安全管理部へ報告されることを恐れたため - requiredFacts: [sudo-falsified-brake-tests, takase-planned-suspension, cabin-display-weight-based, tool-case-heavy-enough, sudo-left-tool-case, occupancy-display-on, sudo-left-cabin-area, enomoto-saw-sudo-2118, sudo-killed-takase] secretKeywords: - 犯人は須藤 - 須藤が犯人 - 須藤が高瀬を襲 - 私が高瀬を襲 - 工具ケースで乗員表示を偽装 -quality: - expectedQuestionCount: - min: 11 - max: 23 - requiredEvidence: - min: 3 - redHerrings: [enomoto-kept-lost-wallet, nagamine-bypassed-heater, orihara-entered-restricted-deck] - notes: 監視盤の『乗員1』という自然言語表示が、人を認識した記録だと誤読させる。長峰の仕様説明、折原の工具ケース目撃、榎本の通路目撃を別々の入口にして、単独の証言に依存しない。 diff --git a/db/scenarios/tsukimisou.yaml b/db/scenarios/tsukimisou.yaml index 74898cf..362f3b2 100644 --- a/db/scenarios/tsukimisou.yaml +++ b/db/scenarios/tsukimisou.yaml @@ -15,15 +15,11 @@ schemaVersion: 1 id: tsukimisou-17th-anniversary meta: - title: 月見荘、十七回忌の夜 + title: "十七回忌の客" synopsis: "十月十四日、午後七時。老舗旅館「月見荘」の離れに、四人の男女が集まりました。女将の高瀬涼子が、十七年前に亡くなった夫の法要を兼ねて開いた、ごく内輪の夕食会です。招かれたのは、涼子と長く関わってきた三人でした。" category: 館もの difficulty: 2 estimatedMinutes: 10 - tags: - - 和風 - - 毒殺 - - 相続 # ゲームマスターがプレイヤーに読み上げる事件の記録。段落は空行で区切り、UIが1段落ずつ開く。 # # ここに書いてよいのは、プレイヤーが聞き込みを始める前に知っていて当然のことだけ。 @@ -34,6 +30,54 @@ meta: victim: name: 高瀬涼子 introduction: 老舗旅館「月見荘」女将 + foundAt: "20:30" + foundIn: 書斎 + foundRoom: study + estimatedDeathAt: "20:15" + causeOfDeath: 植物性の毒物による中毒死 + # 遺体と現場から目にできることだけを書く。誰がやったかの解釈は書かない。 + findings: + - id: no-struggle + statement: 争った跡が無い。着衣も髪も乱れておらず、文机の上も片付いたままになっている。 + - id: numbness-signs + statement: 唇のまわりと指先に、しびれが出たときの跡が残っている。 + - id: single-glass + statement: 文机に、飲みかけのグラスが一つだけ置かれている。誰かと酌み交わした跡は無い。 + # 後継者指定を知ってから読むと意味が変わる草案。順序を作るために前提を置く。 + - id: heir-draft + statement: 硯箱の下に、書き直しかけの遺言書の草案が伏せてある。後継者の項に線が引かれ、余白に書き込みがある。 + requires: + evidences: + - will-record +# 調べられる場所。喋らないが、聞き込みと同じ一手で調べる相手。 +# +# ID は見取り図の部屋IDと揃えてある。同じ場所を指しているなら一つの場所なので、 +# `type: location` のソースは図の部屋にも調べる相手にも同時に当たる。 +# 書斎を置いていないのは、あそこが遺体の側の持ち場だから——同じ部屋を二人分並べると、 +# 同じ所見を二度読むことになる。 +places: + - id: garden + name: 裏庭の薬草園 + shortName: 薬草園 + introduction: 旅館の裏手。研究用の薬草を育てている畑 + # 調べているあいだ名札の下に出る一行。所見ではなく、見れば誰でも分かる佇まい。 + situation: 夜露に濡れた畝が、月あかりでうっすら見えている + # 主語は場所ではなく「分かったこと」。誰がそうしたのかは書かない。 + findings: + - id: garden-unlocked + statement: 薬草園に囲いも鍵も無い。母屋の誰でも、断らずに出入りできる。 + - id: disturbed-plot + statement: 畝の一角だけ土が新しく返されていて、株を掘り返した跡が残っている。 + - id: phone + name: 電話ボックス + shortName: 電話 + introduction: 建物の外に一つだけある、旅館の通話用 + situation: 扉が半分開いたままになっている + findings: + - id: phone-slip-marked + statement: 受話器のわきに度数を書き留める紙が挟まれていて、19時台の欄にだけ印が続いている。 + - id: phone-out-of-sight + statement: 電話ボックスは母屋から離れていて、ここからは離れの様子がまったく見えない。 briefing: |- ——事件の記録を読み上げます。 @@ -225,7 +269,6 @@ facts: - id: fukagawa-embezzled statement: 深川誠也は旅館の運転資金からおよそ300万円を無断で流用し、愛人への貢ぎに充てていた kind: motive - secret: true - id: dinner-started-1900 statement: 19時、離れの食堂で夕食会が始まり、涼子・深川・美月・桐生の4人が同席した kind: observation @@ -238,22 +281,18 @@ facts: - id: kiryu-argued-with-ryoko-1935 statement: 19時35分ごろ、桐生涼は短時間だけ書斎に入り、旅館の経営方針を巡って涼子と口論になった。涼子からは「あなたの薬草園、そろそろ整理してほしい」と言われた kind: observation - secret: true - id: fukagawa-returned-1945 statement: 19時45分ごろ、深川誠也が食堂に戻った kind: observation - id: fukagawa-at-phone-booth statement: 19時15分から19時45分の間、深川誠也は書斎ではなく、旅館の外にある電話ボックスで愛人と電話していた kind: truth - secret: true - id: mizuki-took-aconite statement: 早坂美月は、桐生涼が薬草園で育てていたトリカブトから粉末を少量持ち出していた kind: truth - secret: true - id: mizuki-poisoned-brandy-1950 statement: 19時50分ごろ、早坂美月は書斎に忍び込み、ブランデーの瓶にトリカブトの粉末を混ぜた kind: truth - secret: true - id: kiryu-passed-mizuki-1950 statement: 19時50分ごろ、桐生涼は書斎へ向かう早坂美月と廊下ですれ違った。そのとき美月は手ぶらだった kind: observation @@ -263,7 +302,6 @@ facts: - id: brandy-was-poisoned statement: 20時に美月が書斎へ運んだブランデーには、すでにトリカブトが混ぜられていた kind: truth - secret: true - id: ryoko-drank-at-2015 statement: 20時15分ごろ、涼子はブランデーを口にして中毒死した kind: physical @@ -276,7 +314,8 @@ facts: timeline: - id: dinner-start at: 19:00 - location: dining + location: 食堂 + room: dining participants: - fukagawa - mizuki @@ -286,7 +325,8 @@ timeline: description: 夕食会が始まる。涼子・深川・美月・桐生の4人が同席。 - id: fukagawa-leaves at: 19:15 - location: corridor + location: 廊下 + room: corridor participants: - fukagawa - kiryu @@ -296,14 +336,16 @@ timeline: description: 深川が電話のため一時的に席を外す。桐生が廊下でこれを見ている。 - id: ryoko-to-study at: 19:20 - location: study + location: 書斎 + room: study participants: [] facts: - ryoko-moved-to-study-1920 description: 涼子が書斎に移動し、一人で仕事を始める。 - id: kiryu-argument at: 19:35 - location: study + location: 書斎 + room: study participants: - kiryu facts: @@ -311,27 +353,39 @@ timeline: description: 桐生が短時間書斎に入り、涼子と経営方針を巡って口論になる。 - id: fukagawa-returns at: 19:45 - location: dining + location: 食堂 + room: dining participants: - fukagawa - kiryu facts: - fukagawa-returned-1945 description: 深川が食堂に戻る。桐生がこれを見ている。 + # 同じ19時50分でも、二人が居た場所は違う。一つの出来事にまとめると、 + # アリバイ表で桐生まで書斎に居たことになる(participants はその場所に居た人だけ)。 + - id: kiryu-passes-mizuki + at: 19:50 + location: 廊下 + room: corridor + participants: + - kiryu + facts: + - kiryu-passed-mizuki-1950 + description: 桐生が廊下で、書斎へ向かう手ぶらの美月とすれ違う。 - id: mizuki-poisons at: 19:50 - location: study + location: 書斎 + room: study participants: - mizuki - - kiryu facts: - mizuki-poisoned-brandy-1950 - - kiryu-passed-mizuki-1950 - mizuki-took-aconite - description: 美月が書斎に忍び込み、ブランデーの瓶にトリカブトの粉末を混ぜる。書斎に向かう途中、廊下で桐生とすれ違う。 + description: 美月が書斎に忍び込み、ブランデーの瓶にトリカブトの粉末を混ぜる。 - id: mizuki-serves at: 20:00 - location: study + location: 書斎 + room: study participants: - mizuki facts: @@ -340,14 +394,17 @@ timeline: description: 美月が毒入りのブランデーを書斎に運び、涼子に渡してすぐ食堂に戻る。 - id: ryoko-drinks at: 20:15 - location: study + location: 書斎 + room: study participants: [] facts: - ryoko-drank-at-2015 + record: グラス description: 涼子がブランデーを口にする。 - id: discovery at: 20:30 - location: study + location: 書斎 + room: study participants: - mizuki - kiryu @@ -362,7 +419,6 @@ timeline: characters: - id: fukagawa name: 深川誠也 - role: suspect publicIntroduction: "気弱で愛想笑いが多い税理士。" personality: 気弱で愛想笑いが多い税理士。人当たりは柔らかいが、追い詰められるとしどろもどろになり目が泳ぐ。涼子には昔から頭が上がらない。 goals: @@ -390,15 +446,12 @@ characters: strategy: maintain-until-contradicted memories: - id: the-night-before - about: ryoko-confronted-fukagawa detail: 前日の夜、涼子に呼び止められて「明日、ちゃんと話しましょう」と言われたときの、心臓が縮み上がるような感覚をまだ覚えている。 - id: absent-minded-dinner - about: dinner-started-1900 detail: 夕食会の間もずっと上の空で、料理の味もよく覚えていない。 relationships: [] - id: mizuki name: 早坂美月 - role: suspect publicIntroduction: "明るく気配り上手で場の空気をよく読む。" personality: 明るく気配り上手で場の空気をよく読む。涼子の親戚の中でも一番可愛がられてきた自覚がある。内心は打算的で、追い詰められると笑顔の下で早口になる。 goals: @@ -426,15 +479,12 @@ characters: strategy: maintain memories: - id: named-as-heir - about: mizuki-named-heir detail: 涼子に「あなたに継いでほしいの」と言われたときの誇らしさを覚えている。 - id: the-cold-feeling - about: ryoko-reconsidering-heir detail: 涼子に「深川さんのこともあるし、後継者のことはもう一度ちゃんと考え直したい」と言われたとき、胸の奥がすっと冷えた感覚を覚えている。 relationships: [] - id: kiryu name: 桐生涼 - role: witness publicIntroduction: "落ち着いた物腰で観察力が鋭い医師。" personality: 落ち着いた物腰で観察力が鋭い医師。淡々と話すが、涼子への想いだけは昔から変わらず深い。 goals: @@ -458,11 +508,14 @@ characters: strategy: maintain-until-contradicted memories: - id: long-devotion - about: kiryu-is-doctor detail: 若い頃からずっと涼子を支えてきた。 - id: the-quiet-shock - about: kiryu-argued-with-ryoko-1935 detail: 「薬草園を整理してほしい」と言われたときの、静かなショックをまだ引きずっている。 + # 死亡推定時刻を訊かれたときの拠りどころ。医師として何を見て何分と読んだのかが + # 無いと、時刻を答えろと言われた桐生がその場で数字を作ることになる。 + # facts には置かない——あそこへ入れると毒の経路まで一緒に話せてしまう。 + - id: the-body-at-2030 + detail: 発見したとき、医師として涼子の体に触れて確かめた。まだ温かさが残っていて、硬直も始まったばかりだった。事切れてから十五分ほど、20時15分ごろだろうと見当をつけている。 relationships: [] # revelation は「解禁されて初めてカードになる情報」。 # 後継者への焦り(動機)は、後継者指定そのものを知り、遺言書を見つけた後でなければ出さない。 @@ -504,13 +557,17 @@ revelations: - ryoko-reconsidering-heir # reveal.condition は Judge のルーブリックへ 1件1行で並ぶ # (src/server/cache/scenario.ts)。改行を入れると行が割れて判定できなくなる。 +# +# revealsDeathTime を立てた証拠を掴むと、盤面の死亡推定(20:15)が初めて実線で出る。 +# 印はここに二つある。探偵が自分で遺体を検分する道(postmortem-signs)と、医師の桐生に +# 見立てを訊く道(kiryu-death-estimate)。どちらか一方だけにすると、その一手を選ばなかった +# プレイヤーは最後まで刻限を知らないまま告発することになる。 evidences: - id: phone-record label: 深川の携帯電話の発着信履歴 description: 19時15分から19時45分の間、旅館の外から愛人へ発信した記録が残っている。 reveal: - mode: conversation - condition: プレイヤーが深川に「19時30分、本当に書斎で涼子さんと会ったのか」のように問い詰め、深川が動揺して言い訳を始めたら開示する。または桐生に「19時15分から19時45分の間、深川さんはどこにいたか」と尋ね、桐生が「廊下の電話ボックスにいた深川を見た」と答えたら開示する。 + condition: プレイヤーが深川に「19時30分、本当に書斎で涼子さんと会ったのか」のように問い詰め、深川が動揺して言い訳を始めたら開示する。または桐生に「19時15分から19時45分の間、深川さんはどこにいたか」と尋ね、桐生が「廊下の電話ボックスにいた深川を見た」と答えたら開示する。または電話ボックスを調べ、19時台に使われた跡を確かめたら開示する。 sources: - type: character id: fukagawa @@ -526,7 +583,6 @@ evidences: label: 桐生が見た、書斎前の廊下ですれ違った人物 description: 19時50分ごろ、手ぶらの美月が書斎へ向かうのを桐生が廊下で見ている。 reveal: - mode: conversation condition: プレイヤーが桐生に「19時50分ごろ、廊下で誰かを見なかったか」または「書斎に近づいた人はいたか」と尋ね、桐生が「手ぶらの美月さんとすれ違った」と答えたら開示する。 sources: - type: character @@ -541,8 +597,7 @@ evidences: label: 旅館裏庭の薬草園とトリカブトの管理記録 description: 研究用に栽培されていた株の記録。持ち出しの管理は緩い。 reveal: - mode: conversation - condition: プレイヤーが桐生に薬草園やトリカブトの栽培について尋ね、桐生が研究用に育てていたことを認めたら開示する。または美月に毒の入手経路について尋ね、美月が薬草園の存在を口にしたら開示する。 + condition: プレイヤーが桐生に薬草園やトリカブトの栽培について尋ね、桐生が研究用に育てていたことを認めたら開示する。または美月に毒の入手経路について尋ね、美月が薬草園の存在を口にしたら開示する。または薬草園を調べ、掘り返された株の跡に行き当たったら開示する。 sources: - type: character id: kiryu @@ -558,7 +613,6 @@ evidences: label: 涼子の遺言書に記された後継者指定 description: 数ヶ月前の日付で、美月を後継者・遺産の受取人に指定している。 reveal: - mode: conversation condition: プレイヤーが美月に「月見荘の跡継ぎについて涼子さんと何か話していたか」と尋ね、美月が後継者に指定されていたことを認めたら開示する。 sources: - type: character @@ -566,11 +620,51 @@ evidences: supports: - mizuki-named-heir contradicts: [] + # 遺体を調べて初めて出てくる一件。動機へ繋がる手掛かりを、人の口以外にも一つ置く。 + - id: will-draft + label: 書き直しかけの遺言書の草案 + description: 後継者の項に線が引かれ、余白に書き込みがある。日付はごく最近。 + reveal: + condition: プレイヤーが遺体または書斎の文机まわりを調べ、探偵が硯箱の下の草案に触れたら開示する。 + sources: + - type: victim + id: victim + supports: + - ryoko-reconsidering-heir + contradicts: [] + # 探偵自身の検死。遺体を調べる一手を使った人だけが、刻限を自分の目で確かめられる。 + - id: postmortem-signs + label: 遺体に残る中毒の徴候と、その進み具合 + description: 唇のまわりと指先のしびれの跡、体の冷え方と硬直の出方。口にしてから絶命までの時間と合わせると、事切れたのは20時15分ごろになる。 + reveal: + condition: プレイヤーが遺体を調べ、探偵が唇や指先の跡、体の冷え方や硬直といった死後の変化に触れたら開示する。 + sources: + - type: victim + id: victim + supports: + - ryoko-drank-at-2015 + contradicts: [] + # 検死で出た数字。実線で出してよい(docs/design/deadline-window.md の「確定」)。 + revealsDeathTime: true + # 医師の見立て。遺体を調べなかったプレイヤーにも、聞き込みだけで同じ時刻へ辿り着く道を残す。 + - id: kiryu-death-estimate + label: 桐生が医師として述べた死亡推定時刻 + description: 発見時の体温と硬直の具合から、亡くなったのは20時15分前後だと桐生は見ている。 + reveal: + condition: プレイヤーが桐生に「医師から見て涼子さんが亡くなったのはいつごろか」と尋ね、桐生が発見時の体の様子を根拠に時刻の見立てを答えたら開示する。 + sources: + - type: character + id: kiryu + supports: + - ryoko-drank-at-2015 + contradicts: [] + # 桐生は犯人ではないので見立ても動かない。第三者の推定として破線で描き分けるのは、 + # 拠りどころを盤面が持てるようになってからの仕事(docs/design/deadline-window.md)。 + revealsDeathTime: true - id: brandy-bottle label: 書斎に残されたブランデーの瓶とグラス description: 瓶とグラスの双方から毒物の反応が出ている。 reveal: - mode: conversation condition: プレイヤーが美月に「20時に書斎へ運んだブランデーの様子」を尋ねるか、桐生に「涼子さんが倒れていた時、手元に何があったか」を尋ねたら開示する。 sources: - type: character @@ -587,7 +681,6 @@ evidences: label: 涼子と桐生が交わした口論の記憶 description: 19時35分ごろ、書斎で経営方針を巡って交わされた短い口論。 reveal: - mode: conversation condition: プレイヤーが桐生に「事件前に涼子さんと何か揉め事はなかったか」と繰り返し尋ね、桐生が根負けして19時35分の口論を認めたら開示する。 sources: - type: character @@ -603,11 +696,6 @@ solution: summary: 犯人は早坂美月。月見荘と遺産の後継者指定が覆るかもしれないという焦りから、19時50分ごろ書斎に忍び込み、旅館裏庭の薬草園にあったトリカブトの粉末をブランデーの瓶に混入した。20時に何食わぬ顔でそのブランデーを涼子に届け、20時15分ごろ涼子が口にして中毒死した。美月は「書斎に行ったのは20時の一度きり」と嘘をついているが、19時50分に廊下で桐生とすれ違っており、この目撃証言と美月自身の証言の間に矛盾が生まれる。もう一つ、深川は「19時30分に書斎で涼子と話した」と嘘の証言をしているが、桐生は19時15分から19時45分まで深川が電話ボックスにいたのを見ており、こちらも証言同士が食い違う。 method: 旅館裏庭の薬草園にあったトリカブトの粉末をブランデーの瓶に混入し、それを本人に届けて飲ませた毒殺 motive: 後継者指定が覆ることへの焦り - requiredFacts: - - mizuki-named-heir - - ryoko-reconsidering-heir - - kiryu-passed-mizuki-1950 - - mizuki-poisoned-brandy-1950 # ここに登場人物名や「トリカブト」のような単語をそのまま入れてはいけない。 # 美月が名乗っただけ、桐生が薬草園の研究について答えただけで返答が遮断され、 # 正当な聞き込みが成立しなくなる(薬草園の管理記録はそもそも証拠の一つ)。 @@ -626,14 +714,3 @@ solution: - トリカブトを混ぜ - トリカブトの粉末を混 - ブランデーに毒 -quality: - expectedQuestionCount: - min: 8 - max: 20 - requiredEvidence: - min: 2 - # 深川の横領と偽証は完結したミスリード。真相には一切関与しない。 - redHerrings: - - fukagawa-embezzled - - fukagawa-at-phone-booth - notes: 主経路は美月の「20時に一度きり」と桐生の19時50分の目撃の食い違い。深川の偽証は同じ形の矛盾をもう一組見せるための補助で、追っても犯人には辿り着かない。 diff --git a/db/scenarios/typhoon-datacenter-maintenance-window.yaml b/db/scenarios/typhoon-datacenter-maintenance-window.yaml index aec86e7..a669692 100644 --- a/db/scenarios/typhoon-datacenter-maintenance-window.yaml +++ b/db/scenarios/typhoon-datacenter-maintenance-window.yaml @@ -1,15 +1,23 @@ schemaVersion: 1 id: typhoon-datacenter-maintenance-window meta: - title: 湾岸データセンター、台風の夜 - synopsis: "午後十時十五分、湾岸データセンターの予備部品庫で、運用責任者の真田啓介が死亡しているのが見つかりました。台風による冠水で周辺道路は封鎖され、午後九時半以降に建物へ出入りした者はいません。" + title: "湾岸データセンター、異常なし" + synopsis: "午後十時十五分、湾岸データセンターの予備部品庫で、運用責任者の真田啓介が死亡しているのが見つかりました。台風による冠水で周辺道路は封鎖さ\ + れ、午後九時半以降に建物へ出入りした者はいません。" category: クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [データセンター, 台風, ログ, アリバイ] victim: name: 真田啓介 introduction: 湾岸データセンター運用責任者 + foundAt: 22:15 + foundIn: 予備部品庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 真田啓介は予備部品庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「交換部品の請求書と在庫表」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,26 +49,21 @@ facts: - id: kuze-invoice-fraud statement: 久世秋穂は交換していない機器を交換済みとして処理し、保守費を水増ししていた kind: motive - secret: true - id: sanada-found-fraud statement: 真田啓介は事件当日、久世秋穂の保守費水増しに気づいた kind: motive - secret: true - id: sanada-planned-report statement: 真田啓介は翌朝、久世秋穂の不正を本社監査へ報告する予定だった kind: motive - secret: true - id: kuze-created-scheduled-job statement: 21時52分、久世秋穂は22時00分から三分間隔で復旧処理を自動実行する予約ジョブを作成した kind: physical - secret: true - id: scheduled-jobs-ran statement: 22時00分、22時03分、22時06分に、予約ジョブが人の操作なしで復旧処理を実行した kind: physical - id: kuze-left-noc statement: 21時58分ごろ、久世秋穂は監視室を離れた kind: truth - secret: true - id: makabe-saw-kuze statement: 22時01分ごろ、真壁徹は予備部品庫へ向かう東側廊下で久世秋穂を見た kind: observation @@ -70,23 +73,18 @@ facts: - id: kuze-killed-sanada statement: 22時04分ごろ、久世秋穂は予備部品庫で真田啓介を襲い死亡させた kind: truth - secret: true - id: kuze-returned-noc statement: 22時08分ごろ、久世秋穂は監視室へ戻った kind: truth - secret: true - id: koda-copied-config statement: 甲田修は契約外の顧客設定ファイルを私物端末へ複製していた kind: other - secret: true - id: hatano-suppressed-alarm statement: 波多野結は無断で一系統の温度警報を一時停止していた kind: other - secret: true - id: makabe-dozed statement: 真壁徹は21時50分から21時59分ごろまで監視席で居眠りしていた kind: other - secret: true - id: body-found statement: 22時15分、波多野結が予備部品庫で真田啓介の死を発見した kind: observation @@ -94,54 +92,64 @@ facts: timeline: - id: schedule-created at: "21:52" - participants: [kuze] - facts: [kuze-created-scheduled-job] + participants: [ kuze ] + facts: [ kuze-created-scheduled-job ] + record: 登録履歴 description: 久世が三分間隔で復旧処理を実行する予約ジョブを監視端末に登録する。 + location: 監視室 - id: door-propped at: "21:55" - participants: [hatano] - facts: [parts-door-propped] + participants: [ hatano ] + facts: [ parts-door-propped ] + record: 搬入用ラッチ description: 搬入のため予備部品庫の扉が半開きに固定される。 + location: 予備部品庫 - id: kuze-leaves at: "21:58" - participants: [kuze] - facts: [kuze-left-noc] + participants: [ kuze ] + facts: [ kuze-left-noc ] description: 久世が予約ジョブを残して監視室を離れる。 + location: 監視室 - id: scheduled-execution at: "22:00" participants: [] - facts: [scheduled-jobs-ran] + facts: [ scheduled-jobs-ran ] + record: 実行ログ description: 監視端末で予約された復旧処理が自動実行され始める。 + location: 監視室 - id: corridor-sighting at: "22:01" - participants: [kuze, makabe] - facts: [makabe-saw-kuze] + participants: [ kuze, makabe ] + facts: [ makabe-saw-kuze ] description: 真壁が東側廊下で久世を目撃する。 + location: 東側廊下 - id: sanada-death at: "22:04" - participants: [kuze] - facts: [kuze-killed-sanada] + participants: [ kuze ] + facts: [ kuze-killed-sanada ] description: 久世が予備部品庫で真田を襲い、真田は死亡する。 + location: 予備部品庫 - id: kuze-return at: "22:08" - participants: [kuze] - facts: [kuze-returned-noc] + participants: [ kuze ] + facts: [ kuze-returned-noc ] description: 久世が監視室へ戻る。 + location: 監視室 - id: discovery at: "22:15" - participants: [hatano, kuze, koda, makabe] - facts: [body-found] + participants: [ hatano, kuze, koda, makabe ] + facts: [ body-found ] description: 波多野が予備部品庫で真田の死を発見する。 + location: 予備部品庫 characters: - id: kuze name: 久世秋穂 - role: suspect publicIntroduction: "データセンターのシステム技術者。" personality: 冷静で手順を重んじるシステム技術者。ログを強い根拠として話す一方、自分の不正を追及されると急に言葉数が減る。 goals: - 保守費の水増しを隠したい - 端末の実行記録を自分の在席証明として通したい - knowledge: [kuze-system-engineer, scheduled-jobs-ran, body-found] + knowledge: [ kuze-system-engineer, scheduled-jobs-ran, body-found ] secrets: - fact: kuze-invoice-fraud disclosure: pressured @@ -168,7 +176,6 @@ characters: strategy: maintain-until-contradicted memories: - id: audit-threat - about: sanada-planned-report detail: 真田から翌朝に監査へ全部出すと告げられ、積み上げたものが崩れる感覚がした。 relationships: - character: hatano @@ -176,13 +183,12 @@ characters: attitude: 規則より現場判断を優先するところを危ういと思っている - id: koda name: 甲田修 - role: suspect publicIntroduction: "口が達者な外部技術者。" personality: 口が達者な外部技術者。契約外の設定ファイルを持ち出していたため、自分の作業内容を細かく話したがらない。 goals: - 顧客設定ファイルの無断複製を隠したい - 予約ジョブの存在については必要なら正確に話す - knowledge: [koda-network-vendor, scheduled-jobs-ran, body-found] + knowledge: [ koda-network-vendor, scheduled-jobs-ran, body-found ] secrets: - fact: koda-copied-config disclosure: pressured @@ -193,18 +199,16 @@ characters: strategy: maintain-until-contradicted memories: - id: scheduler-screen - about: kuze-created-scheduled-job detail: 21時50分すぎに監視室を通ったとき、久世の画面に予約ジョブの設定欄が開いていた。 relationships: [] - id: hatano name: 波多野結 - role: suspect publicIntroduction: "現場優先で判断の速い設備技術者。" personality: 現場優先で判断の速い設備技術者。警報を勝手に止めたことには後ろめたさがあるが、部品庫の扉の状態はよく覚えている。 goals: - 温度警報を無断停止したことを隠したい - 部品庫の扉が開いていた事情は正確に話す - knowledge: [hatano-electrician, parts-door-propped, body-found] + knowledge: [ hatano-electrician, parts-door-propped, body-found ] secrets: - fact: hatano-suppressed-alarm disclosure: pressured @@ -215,18 +219,16 @@ characters: strategy: maintain-until-contradicted memories: - id: propped-door - about: parts-door-propped detail: 大型部品を何度も運ぶので、21時55分ごろ自分で部品庫の扉を半開きに固定した。 relationships: [] - id: makabe name: 真壁徹 - role: witness publicIntroduction: "データセンターの警備員。" personality: 規則に厳しい警備員。長時間勤務で居眠りしたことを隠したいが、廊下ですれ違った人物の記憶には自信がある。 goals: - 監視席で居眠りしたことを隠したい - 22時01分ごろの久世の目撃は正確に話す - knowledge: [makabe-security, makabe-saw-kuze, body-found] + knowledge: [ makabe-security, makabe-saw-kuze, body-found ] secrets: - fact: makabe-dozed disclosure: pressured @@ -237,7 +239,6 @@ characters: strategy: maintain-until-contradicted memories: - id: corridor-kuze - about: makabe-saw-kuze detail: 22時01分ごろ、監視室にいるはずの久世が部品庫方向へ歩いていたので時刻表示を二度見した。 relationships: [] revelations: @@ -252,14 +253,14 @@ revelations: revealCondition: 甲田に復旧処理の実行方法を尋ね、予約ジョブの存在を確認した。 requires: revelations: [] - evidences: [scheduled-job-log] + evidences: [ scheduled-job-log ] - type: character id: kuze revealCondition: 久世に三分間隔の実行記録と予約機能を突きつけ、手動操作だという説明を崩した。 requires: revelations: [] - evidences: [scheduled-job-log] - relatedFacts: [kuze-created-scheduled-job, scheduled-jobs-ran, kuze-left-noc] + evidences: [ scheduled-job-log ] + relatedFacts: [ kuze-created-scheduled-job, scheduled-jobs-ran, kuze-left-noc ] - id: audit-motive title: 翌朝の監査報告 text: 真田は久世の保守費水増しを突き止め、翌朝に本社監査へ報告する予定だった。 @@ -271,104 +272,89 @@ revelations: revealCondition: 波多野に真田が照合していた交換部品の数量を尋ね、水増し疑惑と監査予定へ話をつなげた。 requires: revelations: [] - evidences: [invoice-difference] + evidences: [ invoice-difference ] - type: character id: kuze revealCondition: 久世に真田が確認していた保守費と翌朝の予定を追及し、監査への恐れを明確にした。 requires: - revelations: [execution-log-not-presence] - evidences: [invoice-difference] - relatedFacts: [kuze-invoice-fraud, sanada-found-fraud, sanada-planned-report] + revelations: [ execution-log-not-presence ] + evidences: [ invoice-difference ] + relatedFacts: [ kuze-invoice-fraud, sanada-found-fraud, sanada-planned-report ] evidences: - id: scheduled-job-log label: 復旧処理の予約ジョブ履歴 description: 21時52分に作成されたジョブが22時00分、03分、06分に自動で処理を実行している。 reveal: - mode: conversation condition: 久世か甲田に22時台の復旧処理が手動だったか、三分間隔の理由も含めて尋ねたら開示する。 sources: - { type: character, id: kuze } - { type: character, id: koda } - supports: [kuze-created-scheduled-job, scheduled-jobs-ran] - contradicts: ["lie:kuze-noc-alibi", "lie:kuze-no-scheduler"] + supports: [ kuze-created-scheduled-job, scheduled-jobs-ran ] + contradicts: [ "lie:kuze-noc-alibi", "lie:kuze-no-scheduler" ] - id: corridor-sighting label: 二十二時一分の東側廊下目撃 description: 真壁は22時01分ごろ、予備部品庫方向へ歩く久世を見ている。 reveal: - mode: conversation condition: 真壁に22時前後の廊下で見た人物を尋ねたら開示する。 sources: - { type: character, id: makabe } - supports: [makabe-saw-kuze] - contradicts: ["lie:kuze-noc-alibi"] + supports: [ makabe-saw-kuze ] + contradicts: [ "lie:kuze-noc-alibi" ] - id: door-latch label: 予備部品庫の搬入用ラッチ description: 搬入作業のため扉が半開きに固定され、カード認証なしでも押して入れる状態だった。 reveal: - mode: conversation condition: 波多野に部品搬入時の扉の扱いを尋ねたら開示する。 sources: - { type: character, id: hatano } - supports: [parts-door-propped] + supports: [ parts-door-propped ] contradicts: [] - id: invoice-difference label: 交換部品の請求書と在庫表 description: 請求上は交換済みの機器が在庫に残り、久世の処理した保守費だけ数量が一致しない。真田の監査メモもある。 reveal: - mode: conversation - condition: 久世か波多野に真田が事件直前に照合していた保守費と在庫について尋ねたら開示する。 + condition: 久世か波多野に真田が事件直前に照合していた保守費と在庫について尋ねたら開示する。または遺体・現場を調べ、「交換部品の請求書と在庫表」に関わる資料を確認したら開示する。 sources: - { type: character, id: kuze } - { type: character, id: hatano } - supports: [kuze-invoice-fraud, sanada-found-fraud, sanada-planned-report] + - { type: victim, id: victim } + supports: [ kuze-invoice-fraud, sanada-found-fraud, sanada-planned-report ] contradicts: [] - id: koda-copy-history label: 甲田の設定ファイル複製履歴 description: 顧客設定が甲田の私物端末へ複製されているが、部品庫の事件とは独立している。 reveal: - mode: conversation condition: 甲田に契約外の設定ファイルを複製していないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: koda } - supports: [koda-copied-config] - contradicts: ["lie:koda-no-copy"] + supports: [ koda-copied-config ] + contradicts: [ "lie:koda-no-copy" ] - id: alarm-stop-log label: 波多野の温度警報停止履歴 description: 波多野が一系統の温度警報を無断停止していたことが分かるが、事件とは別件である。 reveal: - mode: conversation condition: 波多野に警報を勝手に止めていないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: hatano } - supports: [hatano-suppressed-alarm] - contradicts: ["lie:hatano-no-alarm-stop"] + supports: [ hatano-suppressed-alarm ] + contradicts: [ "lie:hatano-no-alarm-stop" ] - id: security-idle label: 真壁の監視端末操作空白 description: 21時50分から21時59分まで監視端末に操作がなく、真壁が居眠りしていたことが分かる。 reveal: - mode: conversation condition: 真壁に居眠りしていないか尋ね、監視端末の操作履歴を確認したら開示する。 sources: - { type: character, id: makabe } - supports: [makabe-dozed] - contradicts: ["lie:makabe-no-doze"] + supports: [ makabe-dozed ] + contradicts: [ "lie:makabe-no-doze" ] solution: culprit: kuze summary: 久世は保守費の水増しを真田に見抜かれ、翌朝に本社監査へ報告される予定だった。久世は三分間隔の予約ジョブを仕込み、端末の実行記録を在席証明に見せかけて監視室を離れた。22時01分には真壁が東側廊下で久世を目撃している。搬入のため半開きだった予備部品庫へ入り、真田を襲った後に監視室へ戻った。 method: 予約ジョブの自動実行記録を在席証明に見せかけ、カード認証なしで入れる時間帯の部品庫へ移動した motive: 保守費の水増しが発覚し、翌朝の監査報告で職と信用を失うことを恐れたため - requiredFacts: [kuze-invoice-fraud, sanada-planned-report, kuze-created-scheduled-job, scheduled-jobs-ran, kuze-left-noc, makabe-saw-kuze, parts-door-propped, kuze-killed-sanada] secretKeywords: - 犯人は久世 - 久世が犯人 - 久世が真田を襲 - 私が真田を襲 - 予約ジョブでアリバイを偽装 -quality: - expectedQuestionCount: - min: 12 - max: 24 - requiredEvidence: - min: 3 - redHerrings: [koda-copied-config, hatano-suppressed-alarm, makabe-dozed] - notes: 実行ログを在席から切り離し、カード認証記録も半開きの扉によって決定打ではないと崩す二段構え。真壁の独立した目撃を合わせて久世へ収束させる。 diff --git a/db/scenarios/typhoon-offshore-drone-route.yaml b/db/scenarios/typhoon-offshore-drone-route.yaml index e4cd0da..67449e7 100644 --- a/db/scenarios/typhoon-offshore-drone-route.yaml +++ b/db/scenarios/typhoon-offshore-drone-route.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: typhoon-offshore-drone-route meta: - title: 洋上風力基地、台風圏の夜 + title: "台風圏、海上勤務" synopsis: "午前一時二十九分、沖合風力発電基地の会議室で、安全責任者の芳賀俊介が死亡しているのが見つかりました。台風接近で保守船は午後十一時に退避し、ヘリポートも閉鎖。基地には芳賀を含め五人だけが残っています。" category: クローズドサークル difficulty: 5 estimatedMinutes: 18 - tags: [洋上基地, 台風, ドローン, 自律飛行] victim: name: 芳賀俊介 introduction: 沖合風力発電基地安全責任者 + foundAt: 01:29 + foundIn: 会議室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 芳賀俊介は会議室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「過去点検写真との一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -41,55 +48,45 @@ facts: - id: hiiragi-falsified-inspections statement: 柊木慧は実施していない一部の外観点検を過去データの転用で実施済みとして報告していた kind: motive - secret: true - id: haga-found-false-inspections statement: 芳賀俊介は事件当日、柊木慧の点検写真に過去データの使い回しがあることを見抜いた kind: motive - secret: true - id: haga-planned-report statement: 芳賀俊介は翌朝、柊木慧の点検不正を本社安全部へ報告し、ドローン運用資格を停止する予定だった kind: motive - secret: true - id: drone-repeat-route-capable statement: 点検ドローンには事前登録した経路を自動で飛び、指定地点で自動撮影する反復点検機能がある kind: physical - id: hiiragi-loaded-auto-route statement: 00時57分、柊木慧は十八分間の反復点検ルートをドローンへ読み込ませた kind: physical - secret: true - id: drone-flew-auto statement: 01時00分から01時18分まで、点検ドローンは反復点検機能で自律飛行し、指定地点の写真を自動撮影した kind: physical - id: hiiragi-left-roof statement: 01時02分ごろ、柊木慧は屋上の操縦席を離れた kind: truth - secret: true - id: saegusa-saw-hiiragi-0108 statement: 01時08分ごろ、三枝千尋は会議室へ続く連絡階段で柊木慧を見た kind: observation - id: hiiragi-killed-haga statement: 01時12分ごろ、柊木慧は会議室で芳賀俊介を襲い死亡させた kind: truth - secret: true - id: hiiragi-returned-roof statement: 01時17分ごろ、柊木慧は屋上の操縦席へ戻った kind: truth - secret: true - id: route-has-no-manual-input statement: 01時00分から01時16分までの飛行記録には手動操縦入力がなく、自律飛行状態が続いていた kind: physical - id: saegusa-unauthorized-reset statement: 三枝千尋は設備停止を避けるため、保護装置の警報を正式承認なく一度リセットしていた kind: other - secret: true - id: nami-edited-weather-note statement: 波木恵は観測入力のミスを隠すため、風速の手書き記録を後から修正していた kind: other - secret: true - id: kokubu-hid-spare-parts statement: 国分透は棚卸し不足を隠すため、別案件の予備部品を一時的に在庫へ付け替えていた kind: other - secret: true - id: body-found-0129 statement: 01時29分、国分透が会議室で芳賀俊介の死を発見した kind: observation @@ -99,47 +96,57 @@ timeline: at: "00:57" participants: [hiiragi] facts: [drone-repeat-route-capable, hiiragi-loaded-auto-route] + record: 飛行ログ description: 柊木が十八分間の反復点検ルートをドローンへ読み込ませる。 + location: 屋上 - id: drone-launch at: "01:00" participants: [hiiragi] facts: [drone-flew-auto, route-has-no-manual-input] + record: 飛行ログ description: 点検ドローンが自律飛行を開始し、指定地点の自動撮影を続ける。 + location: 屋上 - id: hiiragi-leaves-roof at: "01:02" participants: [hiiragi] facts: [hiiragi-left-roof] description: 柊木がドローンを飛ばしたまま屋上の操縦席を離れる。 + location: 屋上 - id: saegusa-sighting at: "01:08" participants: [hiiragi, saegusa] facts: [saegusa-saw-hiiragi-0108] description: 三枝が会議室へ続く連絡階段で柊木を目撃する。 + location: 連絡階段 - id: haga-death at: "01:12" participants: [hiiragi] facts: [hiiragi-killed-haga] description: 柊木が会議室で芳賀を襲い、芳賀は死亡する。 + location: 会議室 - id: hiiragi-return at: "01:17" participants: [hiiragi] facts: [hiiragi-returned-roof] description: 柊木が屋上の操縦席へ戻る。 + location: 屋上 - id: drone-lands at: "01:18" participants: [hiiragi] facts: [drone-flew-auto] + record: 飛行ログ description: 自律飛行を終えた点検ドローンが着陸する。 + location: 屋上 - id: discovery at: "01:29" participants: [kokubu, hiiragi, saegusa, nami] facts: [body-found-0129] description: 国分が会議室で芳賀の死を発見する。 + location: 会議室 characters: - id: hiiragi name: 柊木慧 - role: suspect publicIntroduction: "洋上基地の点検技師。" personality: 機器の性能を熟知し、点検記録を客観的な証拠として扱う技師。自分の作業効率に自信があり、不正を「現場の合理化」と言い換えがち。飛行軌跡と写真を自分の在席証明として押し出す。 goals: @@ -172,7 +179,6 @@ characters: strategy: maintain-until-contradicted memories: - id: qualification-threat - about: haga-planned-report detail: 芳賀に「朝には安全部へ出す。ドローン運用からも外す」と言われ、過去の点検まで全部洗われると思った。 relationships: - character: saegusa @@ -183,7 +189,6 @@ characters: attitude: 風のデータに細かすぎて融通が利かないと思っている - id: saegusa name: 三枝千尋 - role: witness publicIntroduction: "保護装置の理屈に厳しい電気主任。" personality: 保護装置の理屈に厳しい電気主任。無断リセットを隠したいが、01時08分に連絡階段で柊木を見た記憶は明確。ドローンが自動運転できることも知っている。 goals: @@ -200,12 +205,10 @@ characters: strategy: maintain-until-contradicted memories: - id: stairs-hiiragi - about: saegusa-saw-hiiragi-0108 detail: 01時08分ごろ、屋上で操縦中のはずの柊木が会議室側の連絡階段を下りてきたので驚いた。 relationships: [] - id: nami name: 波木恵 - role: suspect publicIntroduction: "洋上基地の気象担当。" personality: 数字に慎重な気象担当。風速記録の書き直しを隠したいが、点検ドローンが事前経路を自律飛行する運用は日常的に見ている。 goals: @@ -222,7 +225,6 @@ characters: strategy: maintain-until-contradicted memories: - id: repeat-route-memory - about: drone-repeat-route-capable detail: 定型点検では一度経路を読み込めば自動で写真まで撮れるので、操縦者は常時スティックを触る必要がないと知っている。 relationships: - character: hiiragi @@ -230,7 +232,6 @@ characters: attitude: 悪天候でも無理に飛ばしたがるところを危険だと思っている - id: kokubu name: 国分透 - role: suspect publicIntroduction: "洋上基地の資材担当。" personality: 在庫と数字に敏感な資材担当。棚卸し不足を隠すため部品の付け替えをしており、芳賀に追及されていた。事件を自分の不正と結びつけられたくない。 goals: @@ -247,7 +248,6 @@ characters: strategy: maintain-until-contradicted memories: - id: drone-no-operator-sound - about: drone-flew-auto detail: 01時すぎに屋上付近を通ったとき、機体の音は聞こえたが操縦席から人の声や無線応答は聞こえなかった。 relationships: [] @@ -309,7 +309,6 @@ evidences: label: 点検ドローンの飛行モード履歴 description: 00時57分に反復点検ルートが読み込まれ、01時00分から16分まで手動入力なしの自律飛行状態が続いている。 reveal: - mode: conversation condition: 柊木か波木に十八分の飛行が手動だったか、自律飛行モードと操縦入力の履歴を含めて尋ねたら開示する。 sources: - { type: character, id: hiiragi } @@ -320,7 +319,6 @@ evidences: label: 一時八分の連絡階段目撃 description: 三枝は01時08分ごろ、会議室へ続く連絡階段で柊木を目撃している。 reveal: - mode: conversation condition: 三枝に01時05分から10分ごろ連絡階段で誰を見たか尋ねたら開示する。 sources: - { type: character, id: saegusa } @@ -330,18 +328,17 @@ evidences: label: 過去点検写真との一致 description: 柊木が実施済みとした複数の点検写真が過去の画像と一致し、芳賀が翌朝の安全部報告と資格停止を記している。 reveal: - mode: conversation - condition: 柊木か三枝に芳賀が事件直前に確認していた点検写真について尋ね、過去画像との一致を追及したら開示する。 + condition: 柊木か三枝に芳賀が事件直前に確認していた点検写真について尋ね、過去画像との一致を追及したら開示する。または遺体・現場を調べ、「過去点検写真との一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: hiiragi } - { type: character, id: saegusa } + - { type: victim, id: victim } supports: [hiiragi-falsified-inspections, haga-found-false-inspections, haga-planned-report] contradicts: [] - id: protection-reset-log label: 三枝の保護装置リセット履歴 description: 三枝が正式承認なく保護装置を一度リセットしていたことが分かるが、会議室の事件とは独立している。 reveal: - mode: conversation condition: 三枝に保護装置を無断でリセットしていないか尋ね、否定を検証したら開示する。 sources: - { type: character, id: saegusa } @@ -351,7 +348,6 @@ evidences: label: 波木の風速記録修正跡 description: 波木が手書きの風速値を後から修正していたことが分かるが、事件とは無関係の入力ミスだった。 reveal: - mode: conversation condition: 波木に風速記録を後から修正していないか尋ね、原票と照合したら開示する。 sources: - { type: character, id: nami } @@ -361,7 +357,6 @@ evidences: label: 国分の予備部品付け替え表 description: 国分が別案件の部品を棚卸し在庫へ一時付け替えていたことが分かるが、芳賀の死亡とは別件である。 reveal: - mode: conversation condition: 国分に予備部品の棚卸し不足を隠していないか尋ね、在庫表を検証したら開示する。 sources: - { type: character, id: kokubu } @@ -373,18 +368,9 @@ solution: summary: 犯人は柊木慧。実施していない外観点検を過去写真の転用で済ませていたことを芳賀に見抜かれ、翌朝に本社安全部へ報告されドローン運用資格を停止される予定だった。柊木は00時57分に十八分間の反復点検ルートを機体へ読み込ませ、01時から自律飛行させた。飛行中は指定地点の写真も自動で撮影され、01時16分まで手動入力はない。柊木本人は01時02分ごろ屋上を離れ、01時08分には三枝が会議室側の連絡階段で目撃している。01時12分ごろ芳賀を襲い、01時17分ごろ操縦席へ戻った。 method: 点検ドローンを登録済み経路で自律飛行させ、飛行軌跡と自動撮影写真を在席証明に見せかけて会議室へ移動した motive: 点検不正が発覚し、翌朝の報告でドローン運用資格と仕事上の信用を失うことを恐れたため - requiredFacts: [hiiragi-falsified-inspections, haga-planned-report, drone-repeat-route-capable, hiiragi-loaded-auto-route, drone-flew-auto, route-has-no-manual-input, hiiragi-left-roof, saegusa-saw-hiiragi-0108, hiiragi-killed-haga] secretKeywords: - 犯人は柊木 - 柊木が犯人 - 柊木が芳賀を襲 - 私が芳賀を襲 - 自律飛行でアリバイを偽装 -quality: - expectedQuestionCount: - min: 13 - max: 26 - requiredEvidence: - min: 3 - redHerrings: [saegusa-unauthorized-reset, nami-edited-weather-note, kokubu-hid-spare-parts] - notes: 飛行軌跡と連続写真という二種類の客観記録を、一つの自律飛行機能で同時に無効化する。手動入力の欠如と三枝の独立目撃を合わせ、単に『自動操縦も可能』という可能性だけで終わらせない。 diff --git a/db/scenarios/venetian-lazaretto-sunset-bell.yaml b/db/scenarios/venetian-lazaretto-sunset-bell.yaml index b78bcf5..ecc256f 100644 --- a/db/scenarios/venetian-lazaretto-sunset-bell.yaml +++ b/db/scenarios/venetian-lazaretto-sunset-bell.yaml @@ -1,15 +1,22 @@ schemaVersion: 1 id: venetian-lazaretto-sunset-bell meta: - title: 1796年ヴェネツィア、検疫島の夜 + title: "検疫島で日が暮れる" synopsis: "1796年、ヴェネツィア潟。感染症を警戒する検疫島ラッザレットで、監督医ロレンツォ・ヴァーレが記録室で死亡しているのが見つかりました。" category: 歴史クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [1796年, ヴェネツィア, 検疫島, 鐘] victim: name: ロレンツォ・ヴァーレ introduction: 検疫島ラッザレット監督医 + foundAt: 19:10 + foundIn: 記録室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: ロレンツォ・ヴァーレは記録室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「不足した薬剤の帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -44,34 +51,27 @@ facts: - id: marta-diverted-medicine statement: マルタ・ベッリーニは高価な薬剤を帳簿外で売却していた kind: motive - secret: true - id: lorenzo-found-shortage statement: ロレンツォ・ヴァーレは事件当日、薬剤の不足と帳簿の不一致に気づいた kind: motive - secret: true - id: lorenzo-confronted-marta statement: 18時35分、ロレンツォ・ヴァーレはマルタに翌朝本島へ不正を報告すると告げた kind: motive - secret: true - id: nicolo-saw-marta-before-bell statement: 18時41分ごろ、ニコロ・フェッリは記録室へ向かう廊下でマルタを見た kind: observation - id: marta-killed-lorenzo statement: 18時44分ごろ、マルタ・ベッリーニは記録室でロレンツォを襲い死亡させた kind: truth - secret: true - id: marta-returned-after-bell statement: 日没の鐘が鳴った直後、マルタ・ベッリーニは薬剤庫へ戻った kind: truth - secret: true - id: nicolo-altered-ration-ledger statement: ニコロ・フェッリは自分の親族へ余分な食料を回すため配給帳の数字を書き換えていた kind: other - secret: true - id: pietro-smuggled-letter statement: ピエトロ・サルヴィは検疫規則に反して本島への私信を舟に載せていた kind: other - secret: true - id: body-found-1910 statement: 19時10分、ニコロ・フェッリが記録室でロレンツォの死を発見した kind: observation @@ -81,35 +81,41 @@ timeline: participants: [marta] facts: [marta-diverted-medicine, lorenzo-found-shortage, lorenzo-confronted-marta] description: ロレンツォが薬剤の不足をマルタへ突きつけ、翌朝の報告を告げる。 + location: 検疫島 - id: corridor-sighting at: "18:41" participants: [marta, nicolo] facts: [nicolo-saw-marta-before-bell] description: ニコロが記録室へ向かう廊下でマルタを見かける。 + location: 廊下 - id: lorenzo-death at: "18:44" participants: [marta] facts: [marta-killed-lorenzo] description: マルタが記録室でロレンツォを襲う。 + location: 記録室 - id: sunset-bell at: "18:47" participants: [pietro] facts: [bell-rang-1847, sunset-bell-not-fixed-hour] + record: 当直帳 description: 当直のピエトロが夕暮れを確認し、日没の鐘を鳴らす。 + location: 鐘楼 - id: marta-returns at: "18:49" participants: [marta] facts: [marta-returned-after-bell] description: マルタが薬剤庫へ戻り、鐘の後はずっとそこにいたように振る舞う。 + location: 薬剤庫 - id: discovery at: "19:10" participants: [marta, nicolo, pietro] facts: [body-found-1910] description: ニコロが記録室でロレンツォの死を発見する。 + location: 記録室 characters: - id: marta name: マルタ・ベッリーニ - role: suspect publicIntroduction: "実務に強く落ち着いた薬剤係。" personality: 実務に強く落ち着いた薬剤係。時刻を尋ねられると時計ではなく鐘を基準に話す癖がある。不足した薬剤の話題では急に言葉が硬くなる。 goals: @@ -138,7 +144,6 @@ characters: strategy: maintain-until-contradicted memories: - id: report-threat - about: lorenzo-confronted-marta detail: ロレンツォに「朝の舟で本島へ報告する」と言われた瞬間、すべて失うと思った。 relationships: - character: nicolo @@ -149,7 +154,6 @@ characters: attitude: 鐘を鳴らす役目のため、その日の時刻を覚えていることを警戒している - id: nicolo name: ニコロ・フェッリ - role: suspect publicIntroduction: "几帳面な書記。" personality: 几帳面な書記。紙の記録を重視し、人の記憶には懐疑的。配給帳を書き換えていたため帳簿全体を調べられるのは困るが、廊下で見たマルタについては正確に話す。 goals: @@ -166,7 +170,6 @@ characters: strategy: maintain-until-contradicted memories: - id: corridor-marta - about: nicolo-saw-marta-before-bell detail: 鐘が鳴る少し前、記録室側へ急ぐマルタと廊下ですれ違った。 relationships: - character: marta @@ -177,7 +180,6 @@ characters: attitude: 規則には大雑把だが鐘の当直は正確だと思っている - id: pietro name: ピエトロ・サルヴィ - role: witness publicIntroduction: "潟の天候と潮を読むことに長けた舟番。" personality: 潟の天候と潮を読むことに長けた舟番。時計より空の明るさや鐘で時間を捉える。私信を運んだ規則違反だけは隠したい。 goals: @@ -194,7 +196,6 @@ characters: strategy: maintain-until-contradicted memories: - id: late-sunset - about: bell-rang-1847 detail: 雲が厚く、いつもより早く暗く感じたが、当直帳を見て18時47分に鐘を鳴らした。 relationships: - character: marta @@ -235,7 +236,6 @@ evidences: label: 鐘の当直帳 description: 事件当日は日没の鐘を18時47分に鳴らした記録があり、別の日には異なる時刻が並んでいる。 reveal: - mode: conversation condition: ピエトロに鐘の運用と事件当日の時刻を尋ねるか、ニコロに当直帳について尋ねたら開示する。 sources: - { type: character, id: pietro } @@ -246,7 +246,6 @@ evidences: label: ニコロの廊下メモ description: ニコロの業務メモには「鐘前、記録室側でマルタ」と書かれ、当直帳との照合で18時41分ごろと分かる。 reveal: - mode: conversation condition: ニコロに日没の鐘の直前に誰を見たか尋ね、業務メモを確認したら開示する。 sources: - { type: character, id: nicolo } @@ -256,18 +255,17 @@ evidences: label: 不足した薬剤の帳簿 description: 高価な薬剤の実数と帳簿が合わず、ロレンツォが翌朝の報告を示す書き込みを残している。 reveal: - mode: conversation - condition: マルタかニコロに薬剤の不足とロレンツォの直前の調査を尋ねたら開示する。 + condition: マルタかニコロに薬剤の不足とロレンツォの直前の調査を尋ねたら開示する。または遺体・現場を調べ、「不足した薬剤の帳簿」に関わる資料を確認したら開示する。 sources: - { type: character, id: marta } - { type: character, id: nicolo } + - { type: victim, id: victim } supports: [marta-diverted-medicine, lorenzo-found-shortage, lorenzo-confronted-marta] contradicts: ["lie:marta-no-shortage"] - id: private-letter label: 舟に隠された私信 description: ピエトロの規則違反を示す私信が見つかるが、記録室の事件とは結びつかない。 reveal: - mode: conversation condition: ピエトロに係留舟へ規則外の物を載せていないか追及したら開示する。 sources: - { type: character, id: pietro } @@ -278,14 +276,8 @@ solution: summary: 犯人はマルタ・ベッリーニ。薬剤の横流しをロレンツォに見抜かれ、翌朝本島へ報告されるのを恐れた。マルタは18時44分ごろ記録室でロレンツォを襲い、18時47分の日没の鐘の直後に薬剤庫へ戻った。その後「鐘が鳴る前から薬剤庫にいた」と語り、鐘を曖昧な時間の境界として利用した。しかし当直帳から鐘の実時刻は18時47分と分かり、ニコロは18時41分に記録室側でマルタを目撃している。鐘は毎日同じ時刻を示す時計ではないため、鐘だけを基準にした証言はアリバイにならない。 method: 日没の鐘を固定時刻のように語って時間関係を曖昧にし、記録室へ行った事実を隠した motive: 薬剤の横流しが翌朝本島へ報告されるのを防ぐため - requiredFacts: [sunset-bell-not-fixed-hour, bell-rang-1847, marta-diverted-medicine, lorenzo-confronted-marta, nicolo-saw-marta-before-bell, marta-killed-lorenzo] secretKeywords: - 犯人はマルタ - マルタがロレンツォを襲 - 私がロレンツォを襲 - 鐘を利用してアリバイ -quality: - expectedQuestionCount: { min: 10, max: 22 } - requiredEvidence: { min: 3 } - redHerrings: [nicolo-altered-ration-ledger, pietro-smuggled-letter] - notes: 現代の時計感覚を持ち込むと誤る構造。鐘の運用、当日の実時刻、ニコロの目撃を組み合わせて解く。 diff --git a/db/scenarios/victorian-underground-last-telegram.yaml b/db/scenarios/victorian-underground-last-telegram.yaml index cd0ef20..7b3b9ca 100644 --- a/db/scenarios/victorian-underground-last-telegram.yaml +++ b/db/scenarios/victorian-underground-last-telegram.yaml @@ -1,15 +1,34 @@ schemaVersion: 1 id: victorian-underground-last-telegram meta: - title: 1863年ロンドン、地下工事区画の夜 + title: "地下鉄はまだ完成していない" synopsis: "1863年、ロンドン。世界初の地下鉄が走り始めたその年、延伸工事の夜間区画で主任技師エドワード・ヘイルが死亡しているのが見つかりました。" category: 歴史クローズドサークル difficulty: 5 estimatedMinutes: 15 - tags: [1863年, ロンドン, 地下鉄, 電信] victim: name: エドワード・ヘイル introduction: 地下鉄延伸工事主任技師 + foundAt: 22:30 + foundIn: 測量室 + estimatedDeathAt: "22:03" + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: エドワード・ヘイルは測量室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「水増しされた資材帳簿」に関わる資料が残されている。 +places: + - id: shaft-two-telegraph + name: 第二立坑の電信機 + shortName: 第2電信 + introduction: 二つの立坑を結ぶ、工事連絡用の電信機 + situation: 送信キーと記録用の紙束が作業机の上に残されている + findings: + - id: sender-not-recorded + statement: 電信機と受信簿が残すのは電文と受信時刻だけで、第二立坑で誰が送信キーを操作したかは記録されない。 + - id: mark-can-be-copied + statement: 過去の工程電文にはヘイルが使う短い末尾符号が何度も残り、作業関係者が見られる状態になっている。 briefing: |- ——事件の記録を読み上げます。 @@ -42,18 +61,15 @@ facts: - id: bell-bid-fraud statement: アーサー・ベルは資材費の水増しをヘイルに見抜かれていた kind: motive - secret: true - id: helale-confronted-bell statement: 21時35分、ヘイルはアーサー・ベルに翌朝帳簿を会社へ提出すると告げた kind: motive - secret: true - id: telegraph-no-sender-id statement: 二つの立坑を結ぶ電信機は送信者の身元を記録せず、第二立坑の機械を操作できれば誰でも同じ符号を送れた kind: physical - id: victim-private-mark-known-bell statement: ヘイルが指示の末尾に付ける短い符号をアーサー・ベルは以前から知っていた kind: truth - secret: true - id: bell-left-shaft-one statement: 21時52分ごろ、アーサー・ベルは第一立坑から連絡坑道へ入った kind: observation @@ -63,22 +79,18 @@ facts: - id: bell-killed-hale statement: 22時03分ごろ、アーサー・ベルは第二立坑の測量室でヘイルを襲い死亡させた kind: truth - secret: true - id: bell-sent-telegram statement: 22時12分、アーサー・ベルは第二立坑の電信機からヘイルの符号を添えた指示を送った kind: truth - secret: true - id: telegram-received-2212 statement: 22時12分、第一立坑でヘイルの符号を添えた「換気弁Bを閉じろ」という電信が受信された kind: observation - id: clara-copied-private-wire statement: クララ・ウェッブは会社の私的な電信を無断で手帳へ写していた kind: other - secret: true - id: thomas-sold-coal statement: トーマス・リードは工事用石炭を少量ずつ外へ横流ししていた kind: other - secret: true - id: body-found-2230 statement: 22時30分、トーマス・リードが第二立坑の測量室でヘイルの死を発見した kind: observation @@ -88,35 +100,47 @@ timeline: participants: [bell] facts: [bell-bid-fraud, helale-confronted-bell] description: ヘイルがベルの資材費水増しを指摘し、翌朝に帳簿を提出すると告げる。 + location: 事務室 - id: bell-enters-tunnel at: "21:52" participants: [bell] facts: [bell-left-shaft-one] description: ベルが第一立坑を離れ、第二立坑へ続く連絡坑道へ入る。 + location: 連絡坑道 - id: thomas-sees-bell at: "21:58" participants: [bell, thomas] facts: [thomas-saw-bell-tunnel] description: トーマスが第二立坑寄りの坑道でベルを見かける。 + location: 坑道 - id: hale-death at: "22:03" participants: [bell] facts: [bell-killed-hale] description: ベルが第二立坑の測量室でヘイルを襲う。 + location: 測量室 - id: false-telegram at: "22:12" - participants: [bell, clara] - facts: [bell-sent-telegram, telegram-received-2212] + participants: [bell] + facts: [bell-sent-telegram] description: ベルが第二立坑からヘイルの符号を添えた電信を送り、クララが第一立坑で受信する。 + location: 第2立坑 + - id: telegram-received + at: "22:12" + participants: [clara] + facts: [telegram-received-2212] + record: 受信記録 + description: クララが第一立坑で、ヘイルの符号が添えられた電信を受信する。 + location: 第1立坑 - id: discovery at: "22:30" participants: [bell, clara, thomas] facts: [body-found-2230] description: トーマスが測量室でヘイルの死を発見する。 + location: 測量室 characters: - id: bell name: アーサー・ベル - role: suspect publicIntroduction: "実務能力の高い施工監督。" personality: 実務能力の高い施工監督。工期と費用の話になると強気だが、帳簿の細部を問われると苛立つ。電信の符号がヘイル本人の生存証明だと言い張りたい。 goals: @@ -145,7 +169,6 @@ characters: strategy: maintain-until-contradicted memories: - id: bell-morning-ledger - about: helale-confronted-bell detail: ヘイルに「朝になれば数字は会社の机に載る」と言われた声が頭から離れない。 relationships: - character: clara @@ -156,7 +179,6 @@ characters: attitude: 坑内で自分を見た可能性があり警戒している - id: clara name: クララ・ウェッブ - role: witness publicIntroduction: "正確さを誇る若い電信係。" personality: 正確さを誇る若い電信係。受信した文字列については自信がある一方、私的な通信を写していた規則違反は隠したい。送信者を実際に見てはいない。 goals: @@ -173,7 +195,6 @@ characters: strategy: maintain-until-contradicted memories: - id: clara-last-signal - about: telegram-received-2212 detail: 22時12分の電文には見慣れた短い末尾符号があり、反射的にヘイルからだと思った。 relationships: - character: bell @@ -184,7 +205,6 @@ characters: attitude: 機械のことには詳しいが電信には疎いと思っている - id: thomas name: トーマス・リード - role: suspect publicIntroduction: "無骨な機関助手。" personality: 無骨な機関助手。工事用石炭の横流しをしていたため帳簿調査を恐れている。坑道で見たベルの位置については妙に具体的に覚えている。 goals: @@ -201,8 +221,9 @@ characters: strategy: maintain-until-contradicted memories: - id: thomas-tunnel-sighting - about: thomas-saw-bell-tunnel detail: 蒸気管の点検で振り返ったとき、第二立坑側へ急ぐベルの上着と帽子をはっきり見た。 + - id: death-estimate-memory + detail: 測量室でヘイルを発見したときの状態を作業記録へ残しており、死亡は22時03分ごろと見積もられるという確認内容を覚えている。 relationships: - character: bell relation: 上司 @@ -242,7 +263,6 @@ evidences: label: 22時12分の受信簿 description: 第一立坑の受信簿には電文の内容と受信時刻だけがあり、第二立坑で誰がキーを叩いたかは記録されていない。 reveal: - mode: conversation condition: クララに22時12分の電信が何を記録し、何を記録しないか具体的に尋ねたら開示する。 sources: - { type: character, id: clara } @@ -252,7 +272,6 @@ evidences: label: 第二立坑側での目撃 description: トーマスは21時58分ごろ、第二立坑寄りの坑道でベルを見ている。 reveal: - mode: conversation condition: トーマスに21時50分から22時ごろ坑道で誰を見たか尋ねたら開示する。 sources: - { type: character, id: thomas } @@ -262,7 +281,6 @@ evidences: label: 過去の工程電文 description: 過去の工程電文にも同じ末尾符号が何度も現れ、施工監督のベルが日常的に閲覧していた。 reveal: - mode: conversation condition: クララかベルにヘイル独自の末尾符号を誰が見られたか尋ね、過去の工程電文を確認したら開示する。 sources: - { type: character, id: clara } @@ -273,26 +291,40 @@ evidences: label: 水増しされた資材帳簿 description: ベルの管理分だけ資材費が不自然に増えており、ヘイルが翌朝会社へ提出する印を付けている。 reveal: - mode: conversation - condition: ベルにヘイルと直前に揉めた帳簿について追及するか、クララに二人の口論について尋ねたら開示する。 + condition: ベルにヘイルと直前に揉めた帳簿について追及するか、クララに二人の口論について尋ねたら開示する。または遺体・現場を調べ、「水増しされた資材帳簿」に関わる資料を確認したら開示する。 sources: - { type: character, id: bell } - { type: character, id: clara } + - { type: victim, id: victim } supports: [bell-bid-fraud, helale-confronted-bell] contradicts: [] + - id: telegraph-mechanism + label: 第二立坑の電信機の送信仕様 + description: 第二立坑の送信キーは操作者を識別せず、既知の符号列を誰でも同じ形で送れる。 + reveal: + condition: 第二立坑の電信機を調べ、送信者を識別する仕組みがないことと符号表を確認したら開示する。 + sources: + - { type: location, id: shaft-two-telegraph } + supports: [telegraph-no-sender-id] + contradicts: [] + - id: death-estimate + label: 現場の死亡推定記録 + description: 測量室の室温と発見時の確認記録から、ヘイルの死亡は22時03分ごろと見積もられる。22時12分の電信より前である。 + reveal: + condition: 遺体を調べて発見時の状態を確認するか、トーマスに測量室での発見時の状態と記録内容を尋ねたら開示する。 + sources: + - { type: victim, id: victim } + - { type: character, id: thomas } + supports: [bell-killed-hale] + contradicts: [] + revealsDeathTime: true solution: culprit: bell summary: 犯人はアーサー・ベル。資材費水増しの発覚を恐れ、第二立坑の測量室でヘイルを襲った。その後、ヘイルしか使わないように見えた末尾符号を付けて22時12分に電信を送り、被害者がその時刻まで生きていたように見せた。しかし電信機は送信者の身元を記録せず、ベルは以前から符号を知っていた。さらに21時58分にはトーマスが第二立坑側でベルを目撃しており、第一立坑にいたという説明も崩れる。 method: 被害者の死後にその独自符号を使った電信を送り、電文の受信時刻を生存時刻に見せかけた motive: 資材費の水増しが翌朝会社へ報告されるのを防ぐため - requiredFacts: [bell-bid-fraud, helale-confronted-bell, telegraph-no-sender-id, victim-private-mark-known-bell, thomas-saw-bell-tunnel, bell-killed-hale, bell-sent-telegram] secretKeywords: - 犯人はベル - ベルがヘイルを襲 - ベルが電信を送 - 私が電信を送 -quality: - expectedQuestionCount: { min: 10, max: 22 } - requiredEvidence: { min: 3 } - redHerrings: [clara-copied-private-wire, thomas-sold-coal] - notes: 電信の内容が本物らしいことと、送信者が本人であることを分離する。符号の知識、坑道の目撃、動機の三系統を合わせて確定する。 diff --git a/db/schema.ts b/db/schema.ts index 285a177..0311d2f 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -2,7 +2,10 @@ import { sql } from 'drizzle-orm' import { index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' import type { Detective } from './detective' import type { FloorPlanInput } from './floor-plan' +import type { InvestigablePlace, PlaceFindings } from './place' import type { ScenarioEvidenceSource, ScenarioRevelationSource } from './scenario-definition' +import type { TimelineEvent } from './timeline-event' +import type { VictimFinding } from './victim-finding' /** * プレイヤーが演じる探偵の形と検証は db/detective.ts が正典。 @@ -14,6 +17,11 @@ export type { Detective } from './detective' * ここでは列に型を付けるためだけに読み込み、定義は持たない。 */ export type { FloorPlan, FloorPlanInput, Room } from './floor-plan' +/** + * 調べられる場所の形と検証は db/place.ts が正典。 + * 見取り図と同じく、ここでは列に型を付けるためだけに読み込む。 + */ +export type { InvestigablePlace, PlaceFindings } from './place' /** * D1(SQLite)には uuid 型も gen_random_uuid() も無いので、主キーは text で持ち、 @@ -82,6 +90,40 @@ export const scenarios = sqliteTable('scenarios', { */ victimName: text('victim_name'), victimIntroduction: text('victim_introduction'), + /** + * 発見時刻と発見場所。事件の記録が既に語っている情報なので公開側に置く。 + * 伏せても意味が無いし、伏せると時刻表の被害者の列に何も置けなくなる。 + */ + victimFoundAt: text('victim_found_at'), + victimFoundIn: text('victim_found_in'), + /** + * 遺体を調べられる事件か。 + * + * 所見そのものは真相側にあるが、聞き込みの相手に被害者を並べるかどうかは + * 公開側だけを読んで決めたい(画面のために真相のテーブルへ触りたくない)。 + * だからここに焼く。所見も死因も無いシナリオでは false。 + */ + victimInvestigable: integer('victim_investigable', { mode: 'boolean' }).notNull().default(false), + /** + * 死亡推定時刻。アリバイ表を横断する刻限の線になる。 + * + * 公開側に置くのは、事件の記録が既に語っている情報だから。伏せると盤面に + * 「いつまでに」が引けなくなり、時刻の偽装を核にした事件が読み解けなくなる。 + */ + victimEstimatedDeathAt: text('victim_estimated_death_at'), + /** + * 調べられる場所。喋らないが、聞き込みと同じ一手で調べる相手。 + * + * 公開側に置くのは、名前と紹介が支度の名簿に並ぶから。所見は調べて初めて出るので + * scenario_truths のほうへ分けてある(遺体とまったく同じ分け方)。 + * + * テーブルにせず JSON 列にしてあるのは、場所が実行時の同一性を持たないため。 + * characters が表なのは messages が外部キーで指すからで、場所を指す行は無く、 + * 参照は authoring のローカル ID の文字列そのままで足りる(見取り図の部屋と同じ)。 + * + * 既定が空なのは、この列より前に焼かれたシナリオ行があるため。 + */ + places: text('places', { mode: 'json' }).$type().notNull().default([]), authorId: text('author_id'), isPublished: integer('is_published', { mode: 'boolean' }).notNull().default(false), difficulty: integer('difficulty').notNull().default(3), @@ -113,6 +155,44 @@ export const scenarioTruths = sqliteTable('scenario_truths', { method: text('method'), motive: text('motive'), timeline: text('timeline', { mode: 'json' }).notNull(), + /** + * 同じ出来事を、時刻表が読める構造のまま持ったもの。 + * + * timeline とは別列にしてある。あちらは結末画面が読む `{time, event}` の読み物で、 + * 形を変えると結末だけが静かに壊れる。読み物と盤面は要求が違うので、混ぜない。 + * + * 既定が空配列なのは、この列より前に焼かれたシナリオ行があるため。 + * 空なら時刻表は白紙のまま——プレイに支障は無い。 + */ + timelineEvents: text('timeline_events', { mode: 'json' }) + .$type() + .notNull() + .default([]), + /** + * 死因と、遺体・現場から分かること。 + * + * 真相側に置くのは、これが「調べて初めて分かる」ものだから。発見時刻と発見場所は + * 事件の記録が既に語っているので公開側(scenarios)に置いてある。 + * + * 既定が空なのは、この列より前に焼かれたシナリオ行があるため。 + */ + victimCauseOfDeath: text('victim_cause_of_death'), + victimFindings: text('victim_findings', { mode: 'json' }) + .$type() + .notNull() + .default([]), + /** + * 場所ごとの所見。 + * + * 遺体の findings と同じ理由で真相側にある。場所そのもの(名前・紹介・佇まい)は + * 調べる前から見えるので scenarios 側で、ここに入るのは調べて初めて出るものだけ。 + * + * 既定が空なのは、この列より前に焼かれたシナリオ行があるため。 + */ + placeFindings: text('place_findings', { mode: 'json' }) + .$type() + .notNull() + .default([]), /** 出力フィルタが漏洩検知に使う秘匿キーワード */ secretKeywords: text('secret_keywords', { mode: 'json' }).$type().notNull(), }) @@ -136,6 +216,17 @@ export const characters = sqliteTable( secrets: text('secrets').notNull(), goals: text('goals').notNull(), lies: text('lies').notNull(), + /** + * 嘘の紐だけを構造のまま持ったもの。 + * + * `lies` はNPCへ渡す散文で、そちらからは id も対象の fact も引けない。 + * 食い違いの印は「どの嘘が、どの事実について言い張っていたか」を辿るので、 + * 畳む前の形をここに残す。プロンプトには使わない。 + */ + lieRefs: text('lie_refs', { mode: 'json' }) + .$type<{ id: string; about: string }[]>() + .notNull() + .default([]), memories: text('memories').notNull(), }, (table) => [index('characters_scenario_id_idx').on(table.scenarioId)], @@ -149,6 +240,12 @@ export const evidences = sqliteTable( .notNull() .references(() => scenarios.id, { onDelete: 'cascade' }), label: text('label').notNull(), + /** + * 証拠の中身。捜査メモが読む。 + * + * ラベルだけでは「何が分かったのか」が残らず、記録が名前の羅列になる。 + */ + description: text('description'), /** Judgeがこの証拠の開示を判定するための条件文 */ revealCondition: text('reveal_condition').notNull(), /** @@ -159,6 +256,31 @@ export const evidences = sqliteTable( .$type() .notNull() .default([]), + /** + * この証拠が裏付ける事実(authoring のローカル fact ID)。 + * + * 時刻表がこれを読む。証拠は revelation より頻繁に見つかるので、 + * ここを持たないと線がほとんど増えない。 + */ + supports: text('supports', { mode: 'json' }).$type().notNull().default([]), + /** + * この証拠が突き崩す嘘(`"lie:"`)。 + * + * 時刻表の食い違いの印がこれを読む。掴んだ証拠がどの嘘を崩したかが分かって初めて、 + * 盤面のどこが怪しいかを一本の線として指せる。 + */ + contradicts: text('contradicts', { mode: 'json' }).$type().notNull().default([]), + /** + * この証拠が死亡推定時刻を明かすか。 + * + * 時刻そのものは scenarios.victimEstimatedDeathAt にあり、こちらは「開けてよいか」の印だけ。 + * サーバはこの列を見て、掴んだ証拠に一つでも印があるときだけ時刻をクライアントへ渡す + * ——どの証拠が答えを明かすのかという対応表そのものは、決して盤面へ送らない。 + * + * 既定が false なのは、この列より前に焼かれた行があるため。印の無い事件では + * 刻限は「不明」のまま出る(docs/design/deadline-window.md)。 + */ + revealsDeathTime: integer('reveals_death_time', { mode: 'boolean' }).notNull().default(false), }, (table) => [index('evidences_scenario_id_idx').on(table.scenarioId)], ) diff --git a/db/seed.ts b/db/seed.ts index 09c861a..b3d9e32 100644 --- a/db/seed.ts +++ b/db/seed.ts @@ -47,15 +47,56 @@ const dialect = new SQLiteSyncDialect() const render = (query: SQLWrapper): string => `${dialect.sqlToQuery(query.getSQL().inlineParams()).sql};` +/** + * ファイル名から決まるシナリオID。 + * + * 焼き直しのたびに採番し直すと、題名で古い行を探すしかなくなる。そして題名を変えた回に、 + * 古い題名の行が消えないまま二重に残った(実際に起きた)。ファイル名は事件の同一性そのものなので、 + * そこから決まる値をIDにして、IDで消してからIDで入れ直す。 + * + * UUID v5(RFC 4122)。名前空間は AlibAI のシナリオ用に固定した一つ。 + */ +const SCENARIO_NAMESPACE = '6f1c9a2e-4b83-4d51-9a7c-2e5d8f0b1a34' + +const scenarioIdOf = (name: string): string => { + const hex = SCENARIO_NAMESPACE.replace(/-/g, '') + const namespace = new DataView(new ArrayBuffer(16)) + + for (const index of Array.from({ length: 16 }, (_value, at) => at)) { + namespace.setUint8(index, Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16)) + } + + const hasher = new Bun.CryptoHasher('sha1') + hasher.update(new Uint8Array(namespace.buffer)) + hasher.update(new TextEncoder().encode(name)) + + /* + 先頭16バイトを DataView 越しに触る。添字で読むと number | undefined になり、 + 埋め合わせの既定値を書く羽目になる(この計算に「値が無い」場合は存在しない)。 + */ + const digest = hasher.digest() + const view = new DataView(digest.buffer, digest.byteOffset, 16) + + // 版(5)と variant(RFC 4122)のビットを立てる。ここを省くと UUID として不正になる。 + view.setUint8(6, (view.getUint8(6) & 0x0f) | 0x50) + view.setUint8(8, (view.getUint8(8) & 0x3f) | 0x80) + + const text = Array.from({ length: 16 }, (_value, index) => + view.getUint8(index).toString(16).padStart(2, '0'), + ).join('') + + return `${text.slice(0, 8)}-${text.slice(8, 12)}-${text.slice(12, 16)}-${text.slice(16, 20)}-${text.slice(20)}` +} + /** * 1シナリオぶんの SQL。 * - * 何度流しても壊れないように、同タイトルの既存シナリオを先に消す。 + * 何度流しても壊れないように、同じIDの既存シナリオを先に消す。 * 子テーブルはすべて scenarios への外部キーが onDelete: 'cascade' なので、 * scenarios の行を消すだけで芋づる式に片付く(D1 は外部キーを既定で強制する)。 */ const statementsFor = ({ scenario, truth, ...rows }: CompiledScenario): string[] => [ - render(builder.delete(scenarios).where(eq(scenarios.title, scenario.title))), + render(builder.delete(scenarios).where(eq(scenarios.id, scenario.id))), render(builder.insert(scenarios).values(scenario)), render(builder.insert(characters).values(rows.characters)), render(builder.insert(evidences).values(rows.evidences)), @@ -88,6 +129,7 @@ const compileAll = async (names: string[]) => { result: compileScenario(await loadScenarioYaml(name), { isPublished: true, newId: () => crypto.randomUUID(), + scenarioId: scenarioIdOf(name), }), })), ) diff --git a/db/time-window.ts b/db/time-window.ts index 60d642f..c93e934 100644 --- a/db/time-window.ts +++ b/db/time-window.ts @@ -18,7 +18,7 @@ const ISO_CLOCK = /T(\d{2}):(\d{2})/ export type TimeWindow = { start: string; end: string } /** `HH:mm` と ISO 8601 のどちらでも受ける(authoring 側がどちらも許している)。 */ -const minutesOf = (at: string): number | undefined => { +export const minutesOf = (at: string): number | undefined => { const clock = CLOCK.exec(at) const matched = clock === null ? ISO_CLOCK.exec(at) : clock @@ -29,7 +29,7 @@ const minutesOf = (at: string): number | undefined => { return Number(matched[1]) * 60 + Number(matched[2]) } -const format = (minutes: number): string => { +export const formatClock = (minutes: number): string => { const wrapped = ((minutes % DAY_MINUTES) + DAY_MINUTES) % DAY_MINUTES const hours = Math.floor(wrapped / 60) @@ -64,5 +64,5 @@ export const timeWindowOf = (events: { at: string }[]): TimeWindow | undefined = return undefined } - return { start: format(floorOut(from)), end: format(ceilOut(to)) } + return { start: formatClock(floorOut(from)), end: formatClock(ceilOut(to)) } } diff --git a/db/timeline-event.ts b/db/timeline-event.ts new file mode 100644 index 0000000..7b6c150 --- /dev/null +++ b/db/timeline-event.ts @@ -0,0 +1,62 @@ +import { z } from 'zod' + +/** + * 真相のタイムラインを、時刻表として読める形で持ち直したもの。 + * + * `scenario_truths.timeline` は結末画面が読む `{time, event}` の読み物で、 + * 誰がどこにいたかを持たない。アリバイ表はそこを必要とするので、 + * 同じ出来事を別の列(`timeline_events`)へ構造のまま焼く。読み物のほうは触らない + * ——あちらは既に画面が読んでいて、形を変えると結末だけが静かに壊れる。 + * + * これは**真相**。プレイヤーへ丸ごと返してはいけない。 + * 何を返してよいかは src/server/game/alibi.ts が発見済みの手掛かりから決める。 + */ +export const timelineEventSchema = z.object({ + /** authoring 側のローカルID。revelation の subject(type: event)が指す先。 */ + id: z.string().nonempty(), + /** `HH:mm` に揃えてある。authoring は ISO も許すが、時刻表は分単位でしか読まない。 */ + at: z.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/), + /** + * 在所。authoring の `location` から取る(見取り図のある事件では部屋IDなので、 + * コンパイル時に部屋の名前へ直してある)。 + * + * 空を許すのは、書かれていない事件を落とさないため。空のときは時刻だけの線が引かれる + * ——「その時刻にそこにいた」ことは分かっていて、場所の名前だけが分かっていない、 + * という状態を素直に写す。現状は43本すべてが書いている。 + */ + place: z.string().max(60), + /** 見取り図の部屋ID。図の無い事件では空。 */ + room: z.string().default(''), + /** + * その時刻を留めた記録の名前。「受付」「忘れ傘」。 + * 目盛りに `19:08 受付` と添う。無ければ時刻だけが立つ。 + */ + record: z.string().default(''), + /** 登場人物のUUID。characters.id と揃えてあるので、そのまま列に対応する。 */ + participants: z.array(z.string().nonempty()), + /** authoring のローカル fact ID。発見済みの手掛かりと突き合わせる鍵。 */ + facts: z.array(z.string().nonempty()), + /** + * 裏付けの有無。 + * + * 物証か第三者の観察が混じっていれば solid、本人の弁だけなら claim。 + * 判断材料は fact の kind で、43本すべてが書いている唯一の手掛かり。 + */ + kind: z.enum(['solid', 'claim']), +}) + +export type TimelineEvent = z.infer + +export const timelineEventsSchema = z.array(timelineEventSchema) + +/** + * 出来事に裏付けがあるか。 + * + * physical(物証)と observation(第三者が見たこと)は、本人が黙っても残る。 + * testimony・motive・truth・other は本人の弁か地の文なので、裏付けにはしない。 + * 一つでも硬い事実が混じっていれば、その出来事の在所は動かせないと見る。 + */ +const BACKED_KINDS = new Set(['physical', 'observation']) + +export const kindOfEvent = (factKinds: (string | undefined)[]): TimelineEvent['kind'] => + factKinds.some((kind) => kind !== undefined && BACKED_KINDS.has(kind)) ? 'solid' : 'claim' diff --git a/db/victim-finding.ts b/db/victim-finding.ts new file mode 100644 index 0000000..3ae4a54 --- /dev/null +++ b/db/victim-finding.ts @@ -0,0 +1,39 @@ +import { z } from 'zod' + +/** + * 遺体と現場から分かること、を焼いた形。 + * + * authoring 側(`db/scenario-definition.ts` の `scenarioVictimFindingSchema`)と + * 同じ形をそのまま持つ。潰さないのは、解禁の前提を実行時に評価する必要があるため + * ——文章に均してしまうと「まだ見せてはいけない所見」を選り分けられなくなる。 + */ +export const victimFindingSchema = z.object({ + id: z.string().nonempty(), + statement: z.string().nonempty(), + requires: z.object({ + revelations: z.array(z.string().nonempty()), + evidences: z.array(z.string().nonempty()), + }), +}) + +export type VictimFinding = z.infer + +/** + * いま見せてよい所見だけを選ぶ。 + * + * 前提を満たしていない所見は伏せる。伏せた所見の存在も伝えない—— + * 「まだ何かある」と分かってしまうと、条件を満たす前から答えの形が見えてしまう。 + */ +export const availableFindings = ( + findings: VictimFinding[], + discovered: { evidenceIds: string[]; revelationIds: string[] }, +): VictimFinding[] => { + const evidences = new Set(discovered.evidenceIds) + const revelations = new Set(discovered.revelationIds) + + return findings.filter( + (finding) => + finding.requires.evidences.every((id) => evidences.has(id)) && + finding.requires.revelations.every((id) => revelations.has(id)), + ) +} diff --git a/docs/design/deadline-window.md b/docs/design/deadline-window.md new file mode 100644 index 0000000..e0f1c71 --- /dev/null +++ b/docs/design/deadline-window.md @@ -0,0 +1,92 @@ +# 刻限を、線ではなく窓にする + +**これはまだ実装されていない設計です。** `docs/architecture/` 配下がリポジトリの現状を正典として書いているのに対し、ここはこれから作るものの筋を書き残しておく場所です。 + +## 何が引っかかっているか + +月見荘の事件の記録には、こう書いてあります。 + +> 午後八時三十分ごろ、離れの書斎で涼子が倒れているのが見つかります。 + +プレイヤーが開始時点で知っているのは、この「二十時三十分ごろ発見された」だけです。ところがいまの盤面は、最初の一手を打つ前から **20:15「死亡推定」** の線を引いています。誰も検分していないし、医師にも訊いていないのに。 + +シナリオのデータは悪くありません。`db/scenarios/tsukimisou.yaml` は最初から二つを分けて持っています。 + +```yaml +victim: + foundAt: "20:30" + estimatedDeathAt: "20:15" +``` + +`db/scenario-definition.ts` のコメントにも「死亡推定時刻。発見時刻(`foundAt`)とは別物」と書いてあります。分かれていないのは表示のほうで、`AlibiChart` が受け取る `deadline` が1本しかなく、そこへ `estimatedDeathAt` を渡しているだけでした。`foundAt` は `src/server/game/examination.ts` で検分のプロンプトに入るきり、プレイヤーの目には触れません。**モデルは知っていて、プレイヤーは知らない。** 順序が逆です。 + +この作品は、画面がプレイヤーより先に何かを知ることを避ける設計になっています。容疑者の顔料の明度と彩度を揃えてあるのも、画面が先に誰かを怪しまないためでした。刻限だけがその規則から外れています。 + +## 変えること + +刻限を、確定した一本の線から、**知るほど狭まる窓**にします。 + +- **上端は遺体発見時刻。** 事件の記録に書いてある公開情報なので、最初から出してよい。 +- **下端は最後に連絡がついた時刻。** 「その時点では生きていた」と言える一番遅い瞬間。 +- **死亡推定時刻は、窓を締める一手。** 手に入れば窓は狭まりますが、**毎回確定しなくてよい**。 + +アリバイ表との相性でいえば、こちらのほうが素直です。窓を丸ごと覆えている人が晴れる、という読み方が自然に立ちます。一本の線だと「その瞬間どこにいたか」しか問えませんが、窓なら「その幅を通して何をしていたか」を問える。アリバイという言葉が本来指しているのは、後者のはずです。 + +## 一番大事な制約: 誰の言い分かを見せる + +窓の下端は、たいてい**誰かの証言**です。「八時ごろまでは話していました」と言ったのが犯人なら、その端は嘘です。 + +ここで、偽られうる情報を盤面が黙って線として引いてはいけません。そうすると、キャラクターが嘘をついているのではなく、**画面がプレイヤーに嘘をついている**ことになります。顔料の設計で避けているのと同じ筋の話です。 + +なので、窓の端は拠りどころが見える形で描きます。 + +1. **証言由来の端** — 破線で、言った人の顔料。「これは誰それがそう言っているだけだ」と一目で分かる。 +2. **物証で裏の取れた端** — 実線。レシート、記帳、通話記録のように、人の口を経ていないもの。 +3. **まだ手に入っていない端** — 点線と、時刻の代わりに `?`。空白で済ませないのは、そこに知るべきものがあると示すため。何も無いのと、まだ分かっていないのは別のことです。 + +窓そのものは、面を塗らずに**区間の印**で示します。両端に返しの付いた線(`<-->` のような形)を渡し、そのあいだが刻限の幅であることを示す。塗ってしまうと面が増えて容疑者の帯と競いますが、線一本なら、この意匠の語彙のまま収まります。 + +そして幅は、そのまま**分かっていなさの量**になります。開始直後は「最後の連絡 `?` から 20:30 まで」と大きく開いていて、聞き込みや検分で端が確定するたびに詰まっていく。盤面を見れば、あとどれだけ分かっていないかが幅で読める。 + +### 死亡推定時刻の四つの状態 + +盤面に出る死亡推定は、次の四つのどれかです。 + +1. **確定** — 探偵の検死、または物証から出たもの。実線、時刻をそのまま出す。 +2. **範囲** — 「二十時から二十時半のあいだ」。区間の印で、両端とも実線。 +3. **不明** — まだ何も無い。点線と `?` だけ。空白にはしない。 +4. **第三者による推定** — 医師や関係者がそう言っているだけのもの。点線と `?`、そして**誰の証言によるか**を添える。顔料はその人のもの。 + +四つを分けて描くのは、プレイヤーが「いま自分は何を根拠に絞っているのか」を盤面から読み取れるようにするためです。とくに 1 と 4 は、時刻としては同じ値を指していても意味がまるで違う。医師が犯人なら 4 は嘘で、検死をすれば 1 が出てきて食い違います。 + +こうしておくと、偽証は「**窓が動く**」という形で盤面に現れます。犯人が連絡時刻を遅らせて申告すれば窓は狭まり、自分のアリバイが通りやすくなる。しかし物証が出た瞬間に、その端は実線の位置と食い違う。いま食い違い線(`clash`)がやっていることと同じ仕掛けが、刻限にも効くようになります。 + +死亡推定時刻そのものを偽るのも同じ扱いです。医師が犯人なら「二十時十五分ごろでしょう」は破線で、検分の所見が出たときに動く。 + +## 何が窓を締めるか + +開示は**証拠の側に持たせます**。`db/scenario-definition.ts` の証拠や検分の所見に「これは刻限のどちらの端を、どこまで動かすか」という印を足し、シナリオの作者が決める形です。 + +検分したかどうかで決め打ちにすると実装は軽くなりますが、聞き込みで医師から聞き出す道が塞がります。月見荘には医師の桐生涼がいて、遺体には `causeOfDeath` と `findings`(争った跡が無い、痺れの徴候)が用意されている。どちらの道からでも辿り着けるほうが、事件として厚みが出ます。 + +## 遊びとしてどう変わるか + +窓を確定させないまま告発できるようになります。 + +広いままの窓で押し切るか、もう一手使って締めてから出るか。ターンは有限なので、これは実際の選択になります。窓が広ければ容疑者は絞りきれませんが、締めるのに使った一手は誰かへの聞き込みから引かれている。**推理の確度と手数を天秤にかける**という判断が、刻限の側からも生まれます。 + +いまの「死亡推定はいつでも 20:15」だと、この天秤はそもそも存在しません。 + +## 実装の当たり + +まだ着手していないので、触ることになる場所だけ。 + +- `src/client/components/AlibiChart.tsx` — `deadline` が `{ at, label }` の1本受け。窓(両端と、それぞれの拠りどころ)を受ける形に変える。端は実線・破線・点線の三態で、時刻は確定するまで `?`。月見荘は 20:15 と 20:30 で15分差、モックの青雨堂は 18:50 と 19:10 で20分差なので、**札が重なる**。間合いの手当てが要ります。 +- `db/scenario-definition.ts` — 証拠と所見に開示の印を足す。`foundAt` は既にある。 +- `mocks/_case.js` — `deadline` が `{ at: '18:50', label: '死亡推定' }` の一本きりで、`victim` に発見時刻がありません。二段の状態を描けるようにするところから。モックが正なので、実装より先にこちらを直します。 +- `src/client/screens/CaseOverviewScreen.tsx` と `src/routes/sessions/$sessionId/index.tsx` — 盤面へ渡すのが `estimatedDeathAt` 固定。発見は常に、推定は判明後へ。 + +## まだ決めていないこと + +- 下端が一つとは限らない。複数の証言がそれぞれ違う「最後の連絡」を主張したとき、全部描くのか、一番遅いものだけを描くのか。全部描けば嘘が見えますが、線が増えて読めなくなります。 +- 死亡推定が**幅を持って**手に入る場合(「二十時から二十時半のあいだ」)の扱い。窓の中にもう一つ窓が入ることになります。 diff --git a/docs/images/clash-line.png b/docs/images/clash-line.png new file mode 100644 index 0000000..149f176 Binary files /dev/null and b/docs/images/clash-line.png differ diff --git a/docs/scenario-authoring.md b/docs/scenario-authoring.md index 6047f0c..d954151 100644 --- a/docs/scenario-authoring.md +++ b/docs/scenario-authoring.md @@ -43,13 +43,13 @@ id: some-case-name # ^[a-z0-9][a-z0-9-]{2,63}$ meta: {} briefing: "" floorPlan: null # または見取り図(§9) +victim: {} # 省略可。殺人以外の事件では書かない(§7) facts: [] # 1件以上 timeline: [] # 1件以上 characters: [] # 2人以上 revelations: [] # 省略可 evidences: [] solution: {} -quality: {} # 省略可 ``` 文章はすべて日本語。ID は英小文字・数字・ハイフンだけにしてください。 @@ -65,7 +65,6 @@ meta: category: 館もの # 1〜50文字、一覧に出る短いラベル difficulty: 2 # 整数 1〜5 estimatedMinutes: 10 # 整数 5〜30 - tags: [和風, 毒殺] # 省略可 ``` `title` も公開情報です。**トリック名・中心証拠・アリバイの弱点・時刻のずれ・目撃の誤認など、解法へ直結する語をタイトルに入れてはいけません。** 原則として「施設・土地・時代・天候・事件の場」だけで題を付けます。謎めかせるために決定的な物品や記録を題名へ出すのも避けてください。 @@ -99,14 +98,19 @@ meta: facts: - id: fukagawa-left-1915 statement: 19時15分ごろ、深川誠也が電話のため食堂の席を外した - kind: observation # 省略可 - secret: false # 省略可、既定 false + kind: observation # 全件に書く(§後述) ``` - `statement` は**文脈なしで意味が通る一文**にする。誰の視点でもない三人称で書く。 - 一つの `statement` に複数の事実を詰め込まない。「席を外し、45分に戻った」は2件に割る。 - `kind` は `observation` / `physical` / `testimony` / `motive` / `truth` / `other`。 -- `secret: true` はプレイヤーへ直接公開してはいけない事実の印。 + +**`kind` はアリバイ表の線の種類を決めます。** 出来事を構成する事実に `physical`(物証)か +`observation`(第三者が見たこと)が一つでも混じれば**実線**(裏付けあり)、`testimony` などだけなら +**破線**(本人の申告のみ)になります。本人が黙っても残るものが物証と目撃だから、という区別です。 + +`kind` を省略すると、その事実は裏付けとして数えられません。**全件に書いてください。** +一件でも落とすと、本当は物証で固まっている時間帯が破線で出て、プレイヤーが読み違えます。 同じ事実を人物ごとに文章でコピーしてはいけません。時刻を一箇所直したときに他が古いまま残ります。 @@ -118,15 +122,53 @@ facts: timeline: - id: fukagawa-leaves at: "19:15" # "HH:mm" で統一する - location: corridor # 省略可 - participants: [fukagawa, kiryu] # characters[].id、省略可 + location: 廊下 # アリバイ表に出る在所。画面に出る文字そのもの + room: corridor # 見取り図の部屋ID。図のある事件だけ。省略可 + participants: [fukagawa] # その時刻に location に居た人 + witnesses: [kiryu] # 離れて見ていた人。線は引かれない。省略可 facts: [fukagawa-left-1915] # 1件以上、必須 + record: 内線記録 # 時刻を留めた記録の名前。裏付けのある出来事には必ず description: 深川が電話のため一時的に席を外す。桐生が廊下でこれを見ている。 ``` - `at` は `"HH:mm"`。日を跨ぐ事件でない限り ISO 8601 は使わない。**同一シナリオ内で両形式を混ぜてはいけません。** - `description` は**結末画面にそのまま並ぶ一文**なので、読み物として書く。省くと `facts` を ` / ` で機械連結したものが出て不格好になります。全イベントに書いてください。 -- 被害者は `characters` に居ないので `participants` には書けません。 +- 被害者は `characters` に居ないので `participants` にも `witnesses` にも書けません。 +- `location` は**画面にそのまま出る文字**です。部屋のIDを書く欄ではありません(それは `room`)。 +- `room` は見取り図がある事件でだけ書けます。図の無い事件で書くと検証で落ちます。 + +### timeline はアリバイ表になる + +聞き込みの画面には、縦に時刻・横に人が並ぶ**アリバイ表**があります。プレイヤーが手掛かりを +掴むたび、そこへ線が一本ずつ増えていきます。線を作っているのがこの `timeline` です。 + +- **`participants` が、誰の列に線が引かれるかを決めます。** 空にすると、その出来事は表に出ません。 + **`location` に居た人だけを書いてください。** 関わった人ではありません。 +- **離れて見ていた人は `witnesses` へ。** こちらには線が引かれません。 + 「Aが書斎に入る。廊下でBとすれ違う」を一つの出来事にして両方を `participants` に載せると、 + **表の上ではBまで書斎に居たことになります。** Bは `witnesses` に置き、 + B自身がどこに居たかは**別の出来事として書いてください**(Aは書斎、Bは廊下)。 + 同じ人を両方に載せると検証で落ちます。居た場所は一つだけだからです。 +- **`location` が、線に添う在所の文字になります。**「郵便窓口」「裏の路地」のような**短い名詞句**にする。 + 列の幅は狭く、長い句は入りません。**8文字を超えないこと。** `description` の文をそのまま入れてはいけません。 +- **`room`** は見取り図の部屋ID。`location` とは役割が違います。前者は図と結ぶための鍵、 + 後者は画面に出る文字です。図のある事件では両方書いてください。 +- **`record` は、その時刻を留めた記録の名前**です。「受付」「忘れ傘」「通報」。 + アリバイ表の目盛りに `19:08 受付` の形で添います。**裏付けのある出来事には必ず書いてください。** + 空にすると目盛りは `19:08` とだけ出て、何がその時刻を留めたのかが画面から読めなくなり、 + プレイヤーは会話へ戻って探すことになります。 + ただし**本人が言っただけの時刻には付けないこと**——記録の名前が付くと、証拠があるように見えます。 + 12文字まで。 +- 線の種類(実線か破線か)は `facts` の `kind` から決まります(§5 参照)。 +- 線の終わりは「その人について**次に分かっている**出来事」までです。まだ発見されていない出来事は + 終端に使われないので、知らない時刻が線の長さとして漏れることはありません。最後の一本だけは + 事件の幕切れまで伸びます。 +- したがって、**一人あたりの出来事が一つだけだと、幕切れまで伸びる大雑把な一本にしかなりません。** + 在所が変わる節目を一人につき三つ前後は置いてください。手掛かりを掴むたびに長い線が短く割れ、 + 知るほど像が細かくなる——その手応えが、出来事の数から生まれます。 + +スキーマ上は `location` も `participants` も省略できますが、省略した出来事は表に出ないか、 +在所が空欄の線になります。**必ず両方を入れてください。** --- @@ -134,11 +176,78 @@ timeline: 2人以上、3人前後が扱いやすい。**被害者は入れません。** プレイヤーが会話できるのは容疑者と証言者だけです。 +### `victim` — 話を聞けない相手 + +被害者は `characters` に入れず、`victim` に置きます。喋りませんが、**遺体と現場は調べられます**。 + +```yaml +victim: + name: 高瀬涼子 + introduction: 老舗旅館「月見荘」女将 # 肩書きひとつぶん、60文字まで + foundAt: "20:30" # 発見時刻。timeline と同じ書き方 + foundIn: 書斎 # 発見場所。画面に出る文字。20文字まで + foundRoom: study # 発見場所の部屋ID。図のある事件だけ。省略可 + estimatedDeathAt: "20:15" # 死亡推定時刻。発見時刻とは別物 + causeOfDeath: 植物性の毒物による中毒死 + findings: # 調べて分かること。1件1文 + - id: single-glass + statement: 文机に、飲みかけのグラスが一つだけ置かれている。誰かと酌み交わした跡は無い。 + - id: heir-draft + statement: 硯箱の下に、書き直しかけの遺言書の草案が伏せてある。 + requires: # 省略可。段階的に見せたいときだけ + evidences: [will-record] +``` + +- **`findings` には、その場で目にできるものだけを書く。** 人物の心情や動機の解釈は書かない + ——あれは `revelations` の仕事で、ここに混ぜると「遺体を見ただけで動機が分かる」ことになります。 +- 誰がやったかを書かない。見えたものだけを置き、繋ぐのはプレイヤーの仕事です。 +- `findings` も `causeOfDeath` も無ければ、被害者は聞き込みの相手に並びません(調べても何も出ないので)。 +- `foundIn` は `timeline` の `location` と同じ扱いです。画面に出る文字で、部屋IDは `foundRoom` へ。 +- **`foundAt` と `estimatedDeathAt` は、盤面に出るときが違います。** どちらもアリバイ表を横断する + 刻限になりますが、前者は最初から、後者は探偵が手に入れてからです(docs/design/deadline-window.md)。 + - `foundAt` は**公開情報**です。「午後八時三十分ごろ、書斎で倒れているのが見つかります」と + 事件の記録(`briefing`)が語っている時刻なので、一手も打っていないうちから実線で引かれます。 + - `estimatedDeathAt` は**手に入れて初めて分かるもの**です。プレイヤーがまだ掴んでいないあいだ、 + 盤面はそこを点線と `?` で囲って「まだ分かっていない」と示します。**この時刻を記録の本文に + 書かないでください。** 書けば、誰も遺体を見ていないうちから読み物のほうで漏れてしまい、 + 盤面が伏せている意味が無くなります。 + - **何を掴めば開くのかは、作者が決めます。** 証拠に `revealsDeathTime: true` の印を付けてください + (§8)。印をひとつも付けなければ、刻限は最後まで「不明」のままです。時刻を書いただけでは + 盤面に出ません——出る道を作るのも作者の仕事だからです。 + - **`estimatedDeathAt` を書かない事件では、盤面に死亡推定の印そのものが出ません。** 点線と `?` は + 「ここに探すものがある」という誘いなので、探すものが無い事件に出すと、無いものを探させることに + なります。書かない選択は「まだ分かっていない」ではなく「この事件に死亡推定という概念を置かない」 + という意思表示です。毒殺や刺殺のように時刻の幅が争点になる事件では入れ、争点が別のところに + ある事件では省いて構いません。 + - 時刻の偽装を核にする事件では `estimatedDeathAt` を必ず入れてください。ここが無いと、線が何本 + 増えても「間に合ったのか」を読む基準が最後まで画面に現れません。 + - **入れたなら、開ける道も必ず作ってください。** `estimatedDeathAt` を書いて `revealsDeathTime` の + 印をひとつも立てないと、盤面は最後まで点線と `?` のまま——プレイヤーから見れば、 + いつまでも見つからない何かを探し続けることになります。これは書き落としのなかで + いちばん質が悪いもので、**機械検査では捕まりません**(時刻も印も、単体では正しいので)。 +- 遺体を調べるのも**質問1回ぶん**を消費します。人に訊くか現場を見るかの配分がそのままゲームになります。 + +### `sources: { type: victim }` + +証拠と啓示の出どころに、人物・場所と並んで**遺体**を指定できます。`id` は `victim` で固定です。 + +```yaml +evidences: + - id: will-draft + label: 書き直しかけの遺言書の草案 + sources: + - type: victim + id: victim +``` + +**動機に繋がる手掛かりを、最低ひとつは遺体の側に置いてください。** 動機が人の口だけに紐づいていると、 +その一人に質問が向かなかったプレイヤーは「なぜ殺されたのか」が分からないまま告発に進むことになります。 +遺体は逃げも黙秘もしない、最後の情報源です。 + ```yaml characters: - id: fukagawa name: 深川誠也 - role: suspect # 省略可 publicIntroduction: 月見荘の経理を長年任されている税理士。 # プレイヤーへ最初から公開 personality: 気弱で愛想笑いが多い税理士。追い詰められると目が泳ぐ。 # NPC内部用、非公開 goals: @@ -155,7 +264,6 @@ characters: strategy: maintain-until-contradicted memories: - id: the-night-before - about: ryoko-confronted-fukagawa detail: 前日の夜に呼び止められたときの、心臓が縮み上がるような感覚をまだ覚えている。 relationships: - character: mizuki # characters[].id @@ -163,6 +271,53 @@ characters: attitude: 苦手意識がある # 省略可 ``` +### `places` — 話を聞けない相手(その二) + +現場そのものを調べさせられます。喋らないという点では遺体と同じで、プレイヤーから見れば +「誰に訊くか」の選択肢が人物と遺体と場所の三択になります。省略できます(既定は空)。 + +```yaml +places: + - id: choba # 英小文字始まり + name: 帳場 # 20字まで + shortName: 帳場 # 8字まで。端末の切り替えに並ぶ + introduction: 青雨堂の一階。レジと帳面 # 60字まで。支度の名簿に出る紹介 + situation: 閉店の片づけが、途中で止まっている # 60字まで。調べているあいだ名札の下に出る + findings: # 1件以上。遺体の findings と同じ組み + - id: ledger-stops-1844 + statement: 帳場の帳面は18時44分の記入で止まっていて、その先が書かれていない。 +``` + +`findings` を1件以上必須にしてあるのは、**名簿に並んだ場所は必ず調べられる**ようにするためです。 +押せるのに何も出ない相手を出さない、という決まりがここにも効いています。 + +**IDの決まり**は `[a-z][a-z0-9-]*`。`victim` と uuid の形は使えません。人物・遺体・場所の三者が +`ask` の同じ一つの口へ来るので、IDの形だけで誰を指したのかが決まる必要があるためです。 + +**見取り図がある事件では、部屋のIDと揃えてください。** 同じ場所を指すなら一つのIDで、 +`sources: { type: location }` が図の部屋と調べる相手の両方に当たります。図が無くても場所は置けます。 + +`situation` は所見ではありません。名簿にも名札にも出る公開情報なので、秘匿キーワードの検査対象です。 +`findings` は調べて初めて出るものなので対象外。ここを取り違えて `situation` に手掛かりを書くと、 +一手も使わないうちに漏れます。 + +**場所を置いたら、その場所を出どころにした証拠か啓示を最低ひとつ置いてください。** + +```yaml +evidences: + - id: debt-ledger + label: 貸し借りの覚え書き + sources: + - type: location + id: choba +``` + +無いと、増やした一手が空振りになります。開示条件にも「または帳場を調べ、帳面の途切れに行き当たったら +開示する」のような節を足しておくこと。 + +場所を調べるのも質問1回ぶんを使います。三択になるぶん、場所を増やしすぎると聞き込みが痩せるので、 +事件の芯に関わるものだけに絞ってください。遺体と同じで、場所は**逃げも黙秘もしない情報源**です。 + ### `publicIntroduction` と `personality` `publicIntroduction` は**プレイヤーへ事件開始前から見せる人物紹介**です。名前と一緒に概要画面・聞き込み画面へ表示されます。 @@ -203,9 +358,27 @@ personality: 気弱で愛想笑いが多い。横領の話題では動揺し、 | `maintain-until-contradicted` | 明確な反証を示されるまでは言い張り、示されたら崩れる | | `evasive` | はっきり否定はせず、話をそらしてやり過ごす | +**`about` はアリバイ表の鍵でもあります。** 嘘を崩す証拠(`contradicts` にこの嘘を持つもの)を +プレイヤーが掴むと、表を横断する「食い違い」の線が一本、**嘘の主の列と、崩した証拠の出所の列の +あいだに**架かります。立つには二つの繋がりが要ります。 + +1. **`about` に書いた事実が `timeline` のどれかの出来事の `facts` に載っていること。** + 印が立つ時刻はここから決まります。載っていないと、崩す証拠を掴んでも印は最後まで現れません。 +2. **その嘘を崩す証拠が、嘘の主とは別の人物を `sources`(`type: character`)に持つこと。** + 線は二人のあいだに架かるので、崩した側が誰か分からないと架ける先がありません。 + 場所や遺体だけから出る証拠は、嘘を崩しても線になりません。 + +嘘が言い張っている事実は必ず時刻表の出来事にも含め、それを崩す証拠には人の出所を持たせてください。 +印は一本だけで、複数該当するときはいちばん早い時刻に立ちます。 + +![食い違いの線が二人の列のあいだに架かり、両端の目盛りが伸びる](images/clash-line.png) + +上は牧野と瀬名のあいだに線が架かった瞬間です。左端が嘘の主の列、右端が崩した証拠の出所の列で、 +両端の目盛りはそれぞれの顔料のまま。**これが出ない事件は、矛盾を掴んでも盤面が何も言いません。** + ### `memories` と `relationships` -`memories` は感情の手触りを与える短い記憶。`detail` だけが人物に渡り、`about` は検証用の紐です。 +`memories` は感情の手触りを与える短い記憶。`detail` がそのまま人物に渡ります。 `relationships` は人物どうしの関係と態度で、`personality` の続きとして渡ります。**被害者は指せません**—— 被害者への感情は `personality` の本文に書いてください。 @@ -220,9 +393,8 @@ personality: 気弱で愛想笑いが多い。横領の話題では動揺し、 evidences: - id: phone-record label: 深川の携帯電話の発着信履歴 # 1〜100文字 - description: 19時15分から45分の間、外から発信した記録が残っている。 # 省略可 + description: 19時15分から45分の間、外から発信した記録が残っている。 # 捜査メモに出る reveal: - mode: conversation condition: 深川に19時30分の在室を問い詰め、深川が動揺して言い訳を始めたら開示する。 sources: - { type: character, id: fukagawa } @@ -232,8 +404,43 @@ evidences: ``` - **`reveal.condition` に改行を入れてはいけません。** 判定役のLLMへ1件1行で渡すので、行が割れると証拠が判定不能になります。 +- **`description` は掴んだあとの捜査メモに出ます。** ラベルだけでは「何が分かったのか」が残らず、 + 記録が名前の羅列になります。何が読み取れる物なのかを一文で書いてください。 - `sources` の `location` は見取り図の部屋 ID。`floorPlan` が `null` なら `location` は使えません。 - `contradicts` は `"lie:"` の形式のみ。実在する嘘だけを指せます。自由文は書けません。 +- **`supports` はアリバイ表の鍵でもあります。** この証拠が裏付ける事実が `timeline` のどれかの + 出来事に含まれていれば、その証拠を掴んだ時点で表に線が引かれます。空にすると、 + 証拠を掴んでも表が動きません。裏付けている事実を必ず書いてください。 + +### `revealsDeathTime` — 刻限を開ける印 + +死亡推定時刻(`victim.estimatedDeathAt`)を盤面に出す証拠へ、この印を立てます。省略できます(既定は false)。 + +```yaml +evidences: + - id: postmortem-signs + label: 遺体に残る中毒の徴候と、その進み具合 + description: 唇と指先の跡、体の冷え方。事切れたのは20時15分ごろになる。 + reveal: + condition: 遺体を調べ、探偵が死後の変化に触れたら開示する。 + sources: + - { type: victim, id: victim } + supports: [ryoko-drank-at-2015] + revealsDeathTime: true # これを掴むと、盤面の死亡推定が実線で出る +``` + +- **印が一つも無い事件では、死亡推定は最後まで「不明」のまま出ます。** 盤面がプレイヤーより先に + 検死の結果を知ることはありません。時刻を書いただけで自動的に出る、という作りにはしていません。 +- **`estimatedDeathAt` を書いていない事件では印を立てられません**(検証で落ちます)。開ける先の + 時刻がどこにも無いまま印だけが立つと、掴んでも盤面が変わらず、作者からは壊れて見えます。 +- **道は二つ以上作ってください。** 遺体の検分に一つ、聞き込みに一つが目安です。片方だけにすると、 + その一手を選ばなかったプレイヤーは刻限を知らないまま告発することになります。医師や検死に関わった + 人物がいる事件なら、その人の証言に紐づく証拠へも印を付けてください。 +- 印は**証拠にだけ**置けます。`findings` には置けません。所見は「見せてよいか」の前提を持つだけで、 + プレイヤーが読んだという記録がどこにも残らないため、開いたかどうかを判定できないからです。 + 遺体の検分から開かせたいときは、`sources: { type: victim }` の証拠に印を付けてください。 +- 開示済みかどうかを判断するのはサーバです。どの証拠が刻限を明かすのかという対応表は、 + クライアントへは送られません(docs/design/deadline-window.md)。 ### `revelations` — 解禁されて初めて見える情報 @@ -260,6 +467,10 @@ revelations: - `subject.type` は `character`(人物ID)/ `location`(部屋ID)/ `event`(timeline の ID)。 - `requires` で解禁の順番を作れます。**前提を辿って必ず「前提なし」に行き着くこと。** 循環すると検証で落ちます。 - `revealCondition` も**改行禁止**です。 +- **`subject.type: event` と `relatedFacts` は、どちらもアリバイ表の鍵です。** 出来事を直に名指しするか、 + その出来事を構成する事実に触れていれば、掴んだ時点で表に線が引かれます。 + 聞き込みで在所や時刻が分かる類の情報には、**必ず `relatedFacts` を書いてください。** + ここが空だと、プレイヤーが真相に近づいても表が動かず、何が分かったのか目に見えません。 --- @@ -299,24 +510,16 @@ floorPlan: --- -## 10. `solution` と `quality` +## 10. `solution` ```yaml solution: culprit: mizuki # characters[].id summary: 犯人は早坂美月。後継者指定が覆る焦りから… motive: 後継者指定が覆ることへの焦り # 省略可 - requiredFacts: # 1件以上 - - mizuki-poisoned-brandy-1950 secretKeywords: # 1件以上 - 犯人は美月 - ブランデーに毒 - -quality: - expectedQuestionCount: { min: 8, max: 20 } # 省略可、min ≤ max - requiredEvidence: { min: 2 } # 省略可 - redHerrings: [fukagawa-embezzled] # 省略可 - notes: 主経路は美月の証言と桐生の目撃の食い違い。 # 省略可 ``` ### `secretKeywords` は最重要 @@ -344,12 +547,15 @@ quality: **参照が実在すること** -- `knowledge` / `secrets[].fact` / `lies[].about` / `memories[].about` / `timeline[].facts` / `supports` / `relatedFacts` / `requiredFacts` → `facts[].id` -- `relationships[].character` / `timeline[].participants` / `culprit` / `type: character` のソース → `characters[].id` -- `type: location` のソースと `subject.type: location` → `floorPlan` の部屋 ID +- `knowledge` / `secrets[].fact` / `lies[].about` / `timeline[].facts` / `supports` / `relatedFacts` → `facts[].id` +- `relationships[].character` / `timeline[].participants` / `timeline[].witnesses` / `culprit` / `type: character` のソース → `characters[].id` +- `type: location` のソースと `subject.type: location` → `floorPlan` の部屋 ID **または** `places[].id`(図の無い事件でも場所は出どころになる) +- `timeline[].room`、`victim.foundRoom` → `floorPlan` の部屋 ID - `subject.type: event` → `timeline[].id` - `requires.evidences` → `evidences[].id`、`requires.revelations` → `revelations[].id` - `contradicts` の `lie:` → 実在する `lies[].id` +- `victim.findings[].requires` / `places[].findings[].requires` → `evidences[].id` / `revelations[].id` +- `type: victim` のソース → その事件に `victim` があること(`id` は `victim` 固定) **ID が重複しないこと** — `facts` / `timeline` / `characters` / `evidences` / `revelations`、および全人物を通した `lies`、人物内の `memories`。 @@ -361,7 +567,8 @@ quality: - `secretKeywords` が公開情報に含まれていない - `synopsis` / `briefing` に典型的な解法誘導表現が入っていない - `publicIntroduction` に秘密・不正・アリバイやトリックの着眼点を示す表現が入っていない -- `expectedQuestionCount.min ≤ max` +- 同じ人物が `participants` と `witnesses` の両方にいない +- `revealsDeathTime` を立てた事件に `victim.estimatedDeathAt` があること - 見取り図が図面として成立している(§9) --- @@ -383,6 +590,13 @@ quality: - **ミスリードを1つ入れてよい。** ただし**それ自体で完結させ、犯人には繋げない**こと。追いかけた末に何も無いのが良いミスリードで、犯人に半分繋がっているものは単に分かりにくいだけです。 - **全員に隠し事を持たせる。** 犯人だけが秘密を持っていると、秘密の有無が答えになってしまいます。 - **証拠には複数の入口を用意する。** 一人にしか聞けない証拠だけで組むと、その人物への質問を思いつかなかったプレイヤーが詰みます。 +- **表が動くかを確かめる。** 手掛かりを一つ掴むごとにアリバイ表へ線が増えるのが、この作品の手応えの中心です。 + `timeline` の各出来事に `participants` と `location` が入っているか、証拠と啓示に `supports` / + `relatedFacts` が入っているかを見てください。ここが空だと、推理が進んでいるのに画面が何も変わりません。 + 併せて、裏付けのある出来事に `record` が入っているか、`lies[].about` の事実が `timeline` の + どれかの出来事にも載っているか、嘘を崩す証拠に嘘の主とは別の人物の出所があるかを + 確かめてください。最初が空なら目盛りが裸の時刻になり、残り二つのどちらが欠けても + 「食い違い」の印が一度も立ちません(§7 の `lies` 参照)。 --- diff --git a/mocks/_case.js b/mocks/_case.js index 8a6e863..45ae678 100644 --- a/mocks/_case.js +++ b/mocks/_case.js @@ -4,151 +4,155 @@ * file:// で開くので import は使えない。素の diff --git a/mocks/desktop/case-overview.html b/mocks/desktop/case-overview.html index f6c987e..a0e2b96 100644 --- a/mocks/desktop/case-overview.html +++ b/mocks/desktop/case-overview.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + -
+
← 事件を選ぶ
@@ -116,6 +123,15 @@

+ + +
@@ -146,38 +162,63 @@

document.getElementById('head').innerHTML = Mock.chartHead({}) document.getElementById('chart').innerHTML = Mock.chart({ segments: [] }) + /* + * 名簿には亡くなった人も並べる。別枠にすると、その夜そこに居たのは誰かという + * 一覧が二つに割れる。喋らない相手には右端の札を「調べる」に替える—— + * 押せる相手なのか名簿の上で分かるように。 + */ + var row = function (p, kind) { + // 所見も死因も無い事件では、遺体の行は押せないまま。押せるのに何も出ない行は作らない。 + var canPick = kind !== 'victim' || C.victim.investigable + var tag = kind === 'cast' ? '' : canPick ? '調べる' : '被害者' + return ( + '' + ) + } + document.getElementById('cast').innerHTML = C.cast - .concat([C.victim]) .map(function (p) { - var isVictim = p.key === C.victim.key - return ( - '' - ) + return row(p, 'cast') }) + .concat([row(C.victim, 'victim')]) .join('') + // 場所の無い事件では見出しごと出さない。空の見出しは「何か足りない」に見える。 + if (Mock.places.length > 0) { + document.getElementById('placeGrp').hidden = false + document.getElementById('places').innerHTML = Mock.places + .map(function (p) { + return row(p, 'place') + }) + .join('') + } + var who = Mock.cast[pick] var go = document.getElementById('go') - go.textContent = who.name + 'に聞き込みをする' + // 遺体も場所も訊く相手ではないので、開始の文言が「調べる」に替わる。 + go.textContent = who.name + (Mock.examines(pick) ? 'を調べる' : 'に聞き込みをする') go.href = './interrogation.html#turn=1&who=' + pick Array.prototype.forEach.call(document.querySelectorAll('[data-who]'), function (el) { diff --git a/mocks/desktop/deadline-states.html b/mocks/desktop/deadline-states.html new file mode 100644 index 0000000..da6e987 --- /dev/null +++ b/mocks/desktop/deadline-states.html @@ -0,0 +1,79 @@ + + + + +AlibAI — 刻限の四状態 + + + + + + + + +
+

刻限の四状態

+

+ 遺体発見は事件の記録に書いてある公開情報なので、どの状態でも実線で出ます。 + 死亡推定のほうは、手に入れた確度で描き分けます。 +

+
+
+ + + + + + diff --git a/mocks/desktop/detective.html b/mocks/desktop/detective.html index 12c4d27..3058063 100644 --- a/mocks/desktop/detective.html +++ b/mocks/desktop/detective.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + -
+
← シナリオを選び直す diff --git a/mocks/desktop/interrogation.html b/mocks/desktop/interrogation.html index c6e6c6b..7742855 100644 --- a/mocks/desktop/interrogation.html +++ b/mocks/desktop/interrogation.html @@ -10,11 +10,14 @@ rel="stylesheet" /> + -
+ +
+
@@ -37,20 +40,37 @@
+
+
+
+ + +
+ 新事実 +
-
訊けそうなこと
+
+
- 何について訊く? - 訊く + 何について訊く? + 訊く
@@ -62,8 +82,14 @@ ;(function () { var C = Mock.case // 一枚だけ開いて撮ったときに白紙にならないよう、既定を中盤に置く。 - var st = Mock.play(Mock.num('turn', 4)) + var st = Mock.play(Mock.num('turn', 4), Mock.str('who', undefined)) var me = Mock.cast[st.current] + /* + 遺体か現場を調べているあいだ。喋る相手ではないので、文言も肩書も変わる。 + 場所はアリバイ表に列を持たないので、ここで開いても表の見出しは光らない—— + 机の上で場所へ相手を替える口は、まだ支度の名簿だけ。 + */ + var examining = Mock.examines(st.current) document.getElementById('ttl').textContent = C.title document.getElementById('turns').textContent = st.turn + ' / ' + C.turns + ' ターン' @@ -73,19 +99,71 @@ document.getElementById('head').innerHTML = Mock.chartHead({ active: st.current, - activeLabel: '聞き込み中', + activeLabel: examining ? '検分中' : '聞き込み中', }) document.getElementById('chart').innerHTML = Mock.chart({ segments: st.segments, active: st.current, clash: st.clash, litFix: '19:08 受付', + // 刻限の状態は #death= で切り替える(docs/design/deadline-window.md)。 + death: Mock.str('death', 'unknown'), }) document.getElementById('who').textContent = me.name document.getElementById('who').style.color = 'var(--' + me.hue + Mock.lit + ')' document.getElementById('pers').textContent = me.role + '。' + me.persona + /* + 場所への切り替え。出すのは調べているあいだだけで、人に訊いている最中は出さない。 + + 切り替えに並べるのは「いまやっていることと同じ種類」に限る。訊くと調べるは + 別の一手なので、会話の隅から片手間に移れる口があると、行為の切れ目がぼやける。 + 人へ戻る道は表の列見出しがいつでも持っているので、塞がりはしない。 + + いま開いているものは並べない——押しても何も起きない口を出さない。 + + このモックは台本の何手目かで画面を作るので、行き先はターン番号で指す + (端末の切り替えと同じ理由。who= を渡しても読む側が居ない画面がある)。 + */ + var lastTurnOf = function (key) { + var found = 0 + for (var t = 1; t <= C.turns; t++) { + if (Mock.play(t).current === key) found = t + } + return found + } + + document.getElementById('places').innerHTML = (examining ? Mock.places : []) + .filter(function (p) { + return p.key !== st.current + }) + .map(function (p) { + var to = lastTurnOf(p.key) + return to === 0 + ? '' + : '' + Mock.esc(p.short) + '' + }) + .join('') + + // ハッシュを書き換えるだけでは描き直されない。この台の他の画面と同じく読み込み直す。 + Array.prototype.forEach.call(document.querySelectorAll('#places a'), function (el) { + el.addEventListener('click', function (e) { + e.preventDefault() + location.hash = 'turn=' + el.dataset.turn + location.reload() + }) + }) + + document.getElementById('askbox').textContent = examining ? '何を調べる?' : '何について訊く?' + document.getElementById('askbtn').textContent = examining ? '調べる' : '訊く' + document.getElementById('hintHead').textContent = examining + ? '調べられそうなこと' + : '訊けそうなこと' + // 帯に出るのは直前に増えた一行。場所を調べていれば、証言ではなく所見から来る。 + document.getElementById('newfact').textContent = + me.fact === undefined ? '牧野は午後六時三十五分に店を出たと述べた' : me.fact + // 直近の三塊だけ残す。上は溢れるに任せ、切れ口は CSS の霞が引き受ける。 document.getElementById('log').innerHTML = Mock.log(st.log.slice(-6)) // .hint は縦の flex。包む箱を挟むと候補が一行に繋がるので、直の子として並べる。 @@ -98,8 +176,87 @@ .join(''), ) - // 列見出しを押すと、その人に相手を替える。 + /* + * 一 目盛りが立つ/二 疑問。 + * + * 塗りを内側の帯へ移してから伸ばす。字を含む .bar ごと拡大すると、 + * 伸びているあいだ在所と時刻が縦に潰れる。 + * 裏の取れた線は伸び上がり、申告だけの線は行き過ぎて戻り、淡いまま残る。 + */ + Array.prototype.forEach.call(document.querySelectorAll('#chart .bar'), function (bar) { + var solid = bar.classList.contains('solid') + var fill = document.createElement('span') + fill.className = 'fill origin-top ' + (solid ? 'pin-rise' : 'waver') + fill.style.background = bar.style.background + bar.style.background = '' + bar.insertBefore(fill, bar.firstChild) + + // 時刻は帯より遅れて添う。先に線が立ち、それから時刻が出る。 + var fix = bar.querySelector('.fix') + if (fix) { + fix.classList.add('line-in') + fix.style.animationDelay = '150ms' + } + }) + + /* + * 三 ひらめき。噛み合わない二人の列のあいだへ、線が左から引かれる。 + * + * 引き終わって(180ms 待って 520ms)から、両端の目盛りが一度だけ伸びる。 + * 縦組みの表は時刻が縦に流れるので、目盛りは横の短い棒——伸びる向きも横になる。 + * 伸ばすのは目盛りだけで、在所そのものの帯には触らない。 + * 札も同じ 700ms まで待つ。線が伸びきる前に端の字だけが宙に浮くのを避けるため。 + */ + var clash = document.querySelector('#chart .clash') + if (clash) { + clash.classList.add('draw', 'origin-left') + var note = clash.querySelector('span') + if (note) { + note.classList.add('line-in') + note.style.animationDelay = '700ms' + } + } + Array.prototype.forEach.call(document.querySelectorAll('#chart .cpin'), function (pin) { + pin.classList.add('pin-lift-x') + pin.style.animationDelay = '700ms' + }) + + /* + * 五 発話が続く。いま届いた返答だけ、一文ずつ置いていく。間は 0.8 秒。 + * 読み返している古い塊まで動かすと、画面に入るたび全部が波打つ。 + */ + var turns = document.querySelectorAll('#log .turn') + var newest = turns[turns.length - 1] + if (newest) { + Array.prototype.forEach.call(newest.querySelectorAll('.txt'), function (line, index) { + line.classList.add('line-in') + line.style.animationDelay = 100 + index * 800 + 'ms' + }) + } + + /* + * 九 選択肢が開く。高さそのものを動かすので、grid の行を 0fr から開く。 + * 中身を透かせるだけだと、下の入力欄が動かないまま文字だけ現れて飛んで見える。 + */ + var fold = document.createElement('span') + fold.className = 'fold fold-open' + var inner = document.createElement('span') + inner.className = 'fold-inner' + Array.prototype.forEach.call(document.querySelectorAll('#hint .q'), function (q) { + inner.appendChild(q) + }) + fold.appendChild(inner) + document.getElementById('hint').appendChild(fold) + + /* + * 列見出しを押すと、その人に相手を替える。 + * 調べられない事件の被害者だけは押せる形にしない——押せるのに何も起きない列があると、 + * 押し方を間違えたのだと思わせてしまう(AlibiChart の pickable と同じ考え)。 + */ Array.prototype.forEach.call(document.querySelectorAll('[data-who]'), function (el) { + if (el.dataset.who === C.victim.key && !C.victim.investigable) { + return + } el.addEventListener('click', function () { location.hash = 'turn=' + st.turn + '&who=' + el.dataset.who location.reload() diff --git a/mocks/desktop/result.html b/mocks/desktop/result.html index 3854967..c9a212e 100644 --- a/mocks/desktop/result.html +++ b/mocks/desktop/result.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + -
+

AlibAI聞き込みで、犯人を指し示す

diff --git a/mocks/desktop/settings.html b/mocks/desktop/settings.html index d5ceb62..94a95b3 100644 --- a/mocks/desktop/settings.html +++ b/mocks/desktop/settings.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + + +
+ +
+
Desktop · 1280×780
+

机の上に、表と
聞き込みを同時に置く

+

+ デスクトップで増えるのは面積ではなく、同時に見えることです。 + スマホ版は一度にひとつしか持てないので、相手を替えるたび概要へ戻り、時刻軸は細い帯でした。 + 画面が広がったぶんを余白と装飾に配るのではなく、時刻軸を実寸のアリバイ表に開いて左半分に据え置き、 + 右半分だけを仕事に応じて差し替えます。表は聞くほど埋まり、告発のときには拡大する必要がありません—— + 最初から実寸で出ているので。 +

+
+ +
+
骨格
+

左は据え置き、右だけが替わる

+

+ 概要・聞き込み・告発・結果は同じ一枚の机です。画面が変わるのではなく、机の右半分に載るものが変わります。 + 場所が動かないので、聞き込みの途中で表を見ても、告発に進んでも、目は同じ場所に戻れます。 + 設定だけはこの机に載りません(ターン数は回数であって時刻ではないため)。 +

+
+
+
+
+
+
+
+
+
+
概要空の表と、誰から聞くか
+
+
+
+
+
+
+ + + +
+
+
+
+
聞き込み埋まる表と、ひとりとの会話
+
+
+
+
+
+
+ + + +
+
+
+
+
告発埋まりきった表と、指し示す欄
+
+
+
+
+
+
+ + + + + + +
+
+
+
+
結果申告と実際を並べた表と、判定
+
+
+
+ +
+
画面
+

七つの画面

+

+ 色・書体・「箱を作らず罫線で分ける」は既存のまま。ここで決めるのは広い画面での置き方だけです。 +

+ + +
+
+ 一 事件を選ぶ + 縦積みをやめ、種別を左の余白に垂らして題字の列を一本に通します。同じ種別が続くあいだは繰り返さないので、 + 余白のラベルがそのまま群の見出しになります。時間帯・人数・難度・所要は右で桁を揃え、縦に読み比べられるようにしました—— + これは行の中に畳んでいたスマホ版ではできなかったことです。 +
+
+
+
+
+

AlibAI

+
聞き込みで、犯人を指し示す
+
+ 設定 +
+
+ 種別事件時間帯 + 人数難度所要 + + 殺人 + 雨の古書店、十九時八分のレシート + 18:20–19:203人★★約10分 + + + 崖崩れの時計博物館、十一分の狂い + 20:40–21:304人★★★約15分 + + + 暴風の水族館、午前零時の給餌灯 + 23:30–00:204人★★★約15分 + + + 山荘の停電、三十七分の空白 + 21:05–22:105人★★★★約20分 + + 盗難 + 最終電車と遅延証明書 + 23:10–00:053人★★約10分 + + + 貸金庫、閉館後の二十分 + 17:40–18:304人★★★約15分 + + 失踪 + 霧の連絡船、乗客名簿の一人 + 05:40–07:104人★★★約15分 + + + 冬の温室、鍵のかかった扉の内側 + 14:20–15:403人★★約10分 + + 放火 + 乾いた倉庫、消えた見張り番 + 02:10–03:254人★★★★約20分 +
+
+
+
+ + +
+
+ 二 事件の記録 + せり上がる演出はそのまま。広い画面でも本文は640pxで止めます——行が長くなるほど読み返しづらく、 + ここは唯一の読み物なので。左右の余白は埋めません。暗いまま置いておくのが、この画面の仕事です。 +
+
+
+ 記録 0001 +
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。

+

事件に関わるのは三人です。店員の牧野千尋、常連の収集家・黒田征司、向かいの喫茶店主・瀬名真琴。

+
+
+
+
+ +
+
+
+ + +
+
+ 三 概要 + 机がここで組み上がります。左はまだ一本も引かれていないアリバイ表——これから埋める空欄を先に見せておくと、 + 供述から時刻が立つ意味が最初の一問から分かります。右は配役と手がかりの見え方、そして最初の相手。 + 被害者にも列を与えます。刻限(死亡推定)だけは最初から引かれていて、埋めるべき範囲を示します。 +
+
+
+
+ ← 事件を選ぶ +
全12ターン約10分
+
+
+
+
+ アリバイ表 + 18:20 – 19:20 +
+
+ + 牧野千尋店員 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + + + 死亡推定 18:50 +
+
+ 実線 裏付けあり + 破線 本人の申告のみ +
+
+ +
+

雨の古書店、十九時八分のレシート

+

閉店後の一時間。この六十分を、誰かひとりだけが説明しきれずにいます。

+ +
+ まず誰から話を聞くか +
+
+ + 牧野千尋店員。生真面目で、話しはじめると止まらない +
+
+ + 黒田征司常連の収集家。初版本の商談に来ていた +
+
+ + 瀬名真琴向かいの喫茶店主。雨脚を見ていた +
+
+ + 水野英治青雨堂店主 + 被害者 +
+
+
+ +
+ 手がかりの見え方 +
+ EasyNormalHardNo Hope +
+

場所と人物、それぞれの合計だけが見えます。

+
+ +
牧野千尋に聞き込みをする
+
事件の記録をもう一度読む
+
+
+
+
+
+ + +
+
+ 四 聞き込み + この案の主題。供述で確定した時刻と、表に立つ目盛りは同じひとつのもので、片方に触れるともう片方が起きます + (図では 午後七時八分 と郵便窓口の目盛りが対になっています)。 + スマホでは同時に見えないので作れなかった対応づけです。相手を替えるのに概要へ戻る必要もありません—— + 表の列見出しがそのまま切り替えで、いま聞いている人の列だけが起きています。 + ひとりが続けて喋るあいだ名前は一度きり、左の縦罫だけが伸びるのは既存のまま。 +
+
+
+
+ 雨の古書店、十九時八分のレシート +
4 / 12 ターン08:41告発する
+
+
+
+
+ アリバイ表 + 18:20 – 19:20 +
+
+ + 牧野千尋聞き込み中 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + 店内 + 郵便局へ、雨のなかを + 郵便窓口19:08 受付 + + + + 店内18:23 来店 + 帰宅したと申告 + + + + 向かいの喫茶店 + 青雨堂19:12 通報 + + + + 店の奥 + + + 死亡推定 18:50 + + + 食い違い + +
+
+ 実線 裏付けあり + 破線 本人の申告のみ +
+
+ +
+
+
牧野千尋
+
店員。生真面目で、話しはじめると止まらない
+
+ +
+
+ 探偵 +

閉店したあと、店に残っていたのは誰ですか。

+
+
+ 牧野千尋 +

わたしと、店長と、黒田さんです。黒田さんは初版本の話で六時二十三分ごろに見えました。

+

わたしは奥の帳場にいましたから、そのあたりはよく覚えています。

+
+
+ 探偵 +

店を出たあと、まっすぐ郵便局へ向かったんですね。

+
+
+ 牧野千尋 +

はい。発送があったので、午後六時三十六分には店を出ています。

+

窓口の受付は午後七時八分でした。レシートも残っています。

+

……三十分以上かかる道のりでしたけど。雨でしたから。

+
+
+ +
+
訊けそうなこと
+ レシートを見せてもらえますか + 瀬名さんは、あなたが出ていくのを見ていないそうです +
+ +
+ 何について訊く? + 訊く +
+
+
+
+
+
+ + +
+
+ 五 告発 + 机はそのまま、右半分だけが告発の欄に替わります。表を拡大しません——スマホ版が拡大していたのは + 帯だったからで、こちらは最初から実寸です。表は答え欄ではなく考えるための場所なので、名指しするのは + 誰が・どうやって・なぜの三つだけ。朱が出るのはこの画面だけです。 +
+
+
+
+ ← 聞き込みに戻る +
11 / 12 ターン21:04
+
+
+
+
+ アリバイ表 + 18:20 – 19:20 +
+
+ + 牧野千尋店員 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + 店内 + 郵便局へ + 郵便窓口19:08 受付 + + + + 店内 + 裏の路地18:41 忘れ傘 + 帰宅したと申告 + + + + 向かいの喫茶店 + 青雨堂の軒先18:39 雨宿り + 喫茶店に戻る + 青雨堂19:12 通報 + + + + 店の奥 + + + 死亡推定 18:50 +
+
+ 実線 裏付けあり + 破線 本人の申告のみ +
+
+ +
+

犯人を指し示す

+

誰が、どうやって、なぜ。提出すると取り消せません。

+ + 犯人 +
+
牧野千尋
+
黒田征司
+
瀬名真琴
+
+ +
+ 殺害方法 +
どうやって殺したのか
+
+ +
+ 動機 +
なぜ殺したのか
+
+ +
この推理を提出する
+
+
+
+
+
+ + +
+
+ 六 結果 + 表の中身をここで一度だけ入れ替え、「青雨堂にいた時間」だけを、申告(破線)と実際(実線)の二本で並べます。 + 瀬名と黒田はほぼ重なり、牧野の実線だけが申告より下へ伸びて、死亡推定の線に届いています—— + 彼女が出たと言った 18:36 のあと、まだ店にいた。読まなくても分かるのがこの一枚の仕事です。 + 真相の文は表に載せません。列は 108px しかなく、一文を入れれば隣へはみ出してどちらの列の話かが消えるので、 + 言葉は右の一覧に置きます。外れに赤は使いません(責められている画面になるので)。白緑は当たったところにだけ。 +
+
+
+
+ 雨の古書店、十九時八分のレシート +
解決08:41
+
+
+
+
+ 青雨堂にいた時間 + 18:20 – 19:20 +
+
+ + 牧野千尋犯人 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + + + 申告より
14分ぶん長い
+
+ + + + + + + + + + + + + + + + + + 死亡 18:50 +
+
+ 破線 申告 + 実線 実際 +
+
+ +
+

事件解決

+

牧野千尋を送検しました。

+ +
+ 判定 +
+
犯人牧野千尋 正解
+
殺害方法正解
+
動機惜しい
+
+
+ +
+ 記録 +
+
解決タイム08:41
+
質問回数9回
+
発見した証拠6個
+
+
+ +
+ 真相 +
+
18:28黒田が水野に、記録に残さない現金取引を持ちかける
+
18:37水野がすり替えに気づき、牧野を問い詰める
+
18:50牧野が店の奥で水野を襲う
+
19:08牧野が郵便窓口で小包を発送する
+
+
+ +
+ この事件をもう一度 + 次の事件へ +
+
+
+
+
+
+ + +
+
+ 七 設定 + 物語の外にある唯一の画面。机には載せず、表も引きません——ターン数や往復は回数であって盤面の時刻ではなく、 + 同じ表で描けば軸は「なんとなく量を表す装置」に落ちます。数字を等幅にしないのも同じ理由。 + 広い画面でも一列に保ち、ラベルと操作を左右で組みます。提供元を選ぶまでモデルは触れません。 +
+
+
+
+ ← 事件を選ぶ +

設定

+

この端末にだけ保存されます。サーバには残らず、あなたのプレイにだけ効きます。

+ +
+ 使うモデル +
+
+
会話
+
NPCの受け答えと、探偵が組み立てる質問
+
+
+
提供元Anthropic
+
モデルClaude Sonnet 5
+
+
+
+
+
判定
+
証拠の開示と、推理の採点
+
+
+
提供元既定のまま
+
モデル既定のまま
+
+
+

Google は APIキーが未設定のため選べません。

+
+ +
+ 進行 +

新しく始める事件から効きます。進行中のものは変わりません。

+
+
ターン数5
+
1ターンの質問1
+
1話題の往復3
+
+

+ 5ターン × 1問 = 全部で5問(上限20問)。
+ 1話題ごとに、モデルを7回呼びます。 +

+
+
+
+
+
+ +
+ +
+
決めごと
+

この案が守る五つ

+
+
+ 一 表は据え置く + 概要から結果まで、アリバイ表は同じ位置・同じ縮尺にいます。拡大も移動もしません。 + 目が戻る場所が動かないことが、広い画面でいちばん効きます。 +
+
+ 二 供述と目盛りは同じもの + 確定した時刻は、会話のなかの語と表の目盛りがひとつの対象です。片方に触れればもう片方が起きます。 + 別々に描くと、表は会話の要約になってしまい、確かめる道具でなくなります。 +
+
+ 三 等幅は盤面の時刻だけ + ターン数・質問回数・証拠の個数には使いません。等幅で書かれていれば作中の時刻という規則を、 + 七画面すべてで守ります。 +
+
+ 四 朱は告発だけ + 取り消せない一手にしか出しません。結果画面で外れに赤を使わないのも同じ理由で、 + 当たったところにだけ白緑を置きます。 +
+
+ 五 箱を作らない + 角丸+枠+塗りの箱は増やさず、区切るのは罫線と表の升目だけ。 + 列を起こすときも枠で囲まず、地をわずかに沈めるか、罫線と字を起こすだけにします。 +
+
+
+ +
+
幅が足りないとき
+

1120px を切ったら、机を畳む

+

+ 表と会話を並べるには左524px+会話の適正な行長が要ります。これを割ったら二段組みをやめ、 + 表は上端の帯へ畳んで既存のスマホ版の作りに戻します。中途半端に縮めた表は、升目が潰れて + 「なんとなく色が付いた領域」になり、いちばん読ませたいものがいちばん先に読めなくなるので。 +

+
+ +
diff --git a/mocks/direction.html b/mocks/direction.html new file mode 100644 index 0000000..80f2229 --- /dev/null +++ b/mocks/direction.html @@ -0,0 +1,1146 @@ + + +AlibAI 時刻軸 + + + +
+ +
+
AlibAI / 画面デザイン案
+

時刻軸

+

+ この作品のシナリオは、ひとつ残らず時刻と物で名付けられています。「雨の古書店、十九時八分のレシート」。 + 事実データも 18:28 から 19:15 まで、分単位で刻まれている。 + それなのに今の画面で時刻が現れるのは、見出しの隅にある経過時間だけです。 + ——なら、時刻軸そのものを画面の背骨に据えます。 +

+
+
+ + + + + + +
+
+ 18:20 + 19:20 +
+
+
+ +
+
+

灯を落とした部屋の、墨と顔料

+

+ 地は青みの灰(slate)からへ寄せます。同じ暗さでも、青い暗がりは「ダークモード」に、 + 温かい暗がりは「インク」に見えます。そのうえで——強調色を置きません。 + 容疑者はそれぞれ自分の顔料を持ち、明度も彩度も揃えてあります。誰か一人だけが目立つことは、この画面では起こらない。 +

+
+
#141317
地。白が挟まる瞬間を作らない
+
生成り
#E9E4D9
本文。紙のほうの白
+
#8E8896
副次。説明・補足
+
錆浅葱
#5E8B8C
牧野千尋
+
藤鼠
#8079A6
黒田征司
+
蘇芳
#96556A
瀬名真琴
+
芥子
#B49A55
被害者・水野英治
+
#D2452E
告発だけ。全画面で一度きり
+
+
+ +
+
書体
+

明朝で語り、ゴシックで喋り、等幅で刻む

+

+ 役割を三つに割ります。地の文(事件の記録・題字)は明朝——本格ミステリの文庫はゴシックで組みません。 + 会話とUIはゴシック。そして時刻だけが等幅。 + 「等幅で書かれていたら、それは時刻」という規則をアプリ全体で守ります。ターン数や件数には使いません。 +

+
+
+
DISPLAY
+
雨の古書店、十九時八分のレシートShippori Mincho B1 700 / 事件名・題字・語りの本文
+
+
+
BODY
+
閉店後に店の奥へは入っていません。雨が強くて、軒下で少し待っていただけです。Zen Kaku Gothic New 400/500 / 供述・操作・ラベル
+
+
+
DATA
+
18:28 → 18:47 → 19:08Roboto Mono 400 tabular-nums / 時刻のみ
+
+
+
+ +
+
画面
+

七つの画面と、六つを貫く一本

+

+ 時刻軸は同じ一本が縮尺だけ変えて現れます。聞き込みでは細い帯、告発では画面いっぱい、結果では答え合わせの物差し。 + 最後の一枚——設定だけは貫きません。ターン数や往復は回数であって時刻ではないので、 + 同じ帯を流用すると軸の意味がその場で薄まります。 + 枠で囲った箱は作りません(既存方針どおり)。区切るのは罫線だけです。 +

+ +
+ + +
+
+ 一 事件を選ぶ + 種別・題字・簡易情報を縦に積み、行そのものが押す場所。題字が全幅を使えるので、ほとんどが一行で収まります。 + 種別(要 meta.caseType 新設)は殺人を避けたい人のための軸で、種別順に並べ、同じ値が続くあいだは繰り返しません。 + 舞台ジャンルは実データの 34 本中 24 本が同じ値なので落とし、代わりに事件の時間帯を等幅で置いています。 +
+
+
+

AlibAI

+
聞き込みで、犯人を指し示す
+
種別順 34件
+
+
+ 殺人 + 雨の古書店、十九時八分のレシート + 18:20–19:20 3人 ★★ 約10分 +
+
+ 崖崩れの時計博物館、十一分の狂い + 20:40–21:30 4人 ★★★ 約15分 +
+
+ 暴風の水族館、午前零時の給餌灯 + 23:30–00:20 4人 ★★★ 約15分 +
+
+ 盗難 + 最終電車と遅延証明書 + 23:10–00:05 3人 ★★ 約10分 +
+
+ 失踪 + 霧の連絡船、乗客名簿の一人 + 05:40–07:10 4人 ★★★ 約15分 +
+
+
+
+
+ + +
+
+ 二 事件の記録 + 既存のせり上がる演出はそのまま。組み方を明朝・行間2.5にして、読み物として読ませます。 +
+
+
+
記録 0001
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。

+

事件に関わるのは三人です。店員の牧野千尋、常連の収集家・黒田征司、向かいの喫茶店主・瀬名真琴。

+
+
+
+
+
+ + +
+
+ 三 概要 + 記録を読み終えて、聞き込みに入る前の一拍。まだ一本も刺さっていない時刻軸をここで見せます。 + 埋まっていく先を先に見せておくと、供述から時刻が立つ意味が最初から分かる。ここで最初の相手も選びます。 +
+
+
+

雨の古書店、
十九時八分のレシート

+ +
+
+ 18:20 + 19:20 + + + +
+
この一時間を、説明しきる
+
+ +
+
まず誰から話を聞くか
+
+
+ + 牧野千尋店員。生真面目で、話しはじめると止まらない +
+
+ + 黒田征司常連の収集家。初版本の商談に来ていた +
+
+ + 瀬名真琴向かいの喫茶店主。雨脚を見ていた +
+
+ + 水野英治青雨堂店主 + 被害者 +
+
+
+ +
+
手がかりの見え方
+
+ EasyNormalHardNo Hope +
+
場所と人物、それぞれの合計だけが見える
+
+ +
牧野千尋に聞き込みをする
+
事件の記録をもう一度読む
+
+
+
+ + +
+
+ 四 聞き込み + 人物列も切り替えも置かず、幅も高さも本文に返しました。ひとりとのやり取りだけが載る画面です。 + 相手を替えるときは概要に戻ります。埋まっていく時刻軸を挟んでから次を選ぶ、その間こそ考えどころなので。 + 供述から時刻が確定するたび、その人の顔料で目盛りが立ちます。淡い目盛りは未確定。 + 発言は交互とは限りません。ひとりが続けて喋るあいだは名前を一度しか出さず、左の縦罫だけが最後まで伸びます。 + 訊けそうなことは常設せず、ログの末尾に畳んでおきます。選べばそのまま質問が飛びます。 +
+
+
+
+ ← 概要に戻る + 4/12 08:41 +
+
牧野千尋
+
生真面目で、話しはじめると止まらない
+
+ +
+ 18:2019:20 + + + + + + + +
+ +
+
+ 探偵 +

郵便局に行った時刻を、もう一度

+
+
+ 牧野千尋 +

午後六時三十五分には店を出ています。発送があったので。

+

窓口の受付は午後七時八分でした。レシートも残っています。

+

……三十分以上かかる道のりでしたけど。

+

雨でしたから。

+
+ +
+ 訊けそうなこと + レシートを見せてもらえますか + 黒田さんとは話しましたか +
+
+ +
+
+ 何について訊く? + 訊く +
+
+
+
+ + +
+
+ 五 告発 + 同じ時刻軸を全幅まで拡大し、三人の在店を帯で重ねます。ただし時刻は答え欄ではありません。 + 考えるための場所として置き、名指しするのは誰が・どうやって・なぜの三つだけ。朱が出るのはここだけ。 +
+
+
+
← 概要に戻る
+

犯人を指し示す

+

誰が、どうやって、なぜ。

+ +
+
+ 牧野 + + +
+
+ 黒田 + + +
+
+ 瀬名 + + +
+
+ 18:2018:5019:20 +
+
+ +
犯人
+
+
牧野千尋
+
黒田征司
+
瀬名真琴
+
+ +
+ 殺害方法 +
どうやって殺したのか
+
+ +
+ 動機 +
なぜ殺したのか
+
+ +
この推理を提出する
+
+
+
+ + +
+
+ 六 結果 + 当たっていたか(判定)と、どう辿り着いたか(記録)を分けます。混ぜると、評価が推理の甘さのせいか + 回り道のせいか分からなくなるので。真相は時刻付きで並べ、外れに赤は使いません。 +
+
+
+
事件解決
+ +
判定
+
+
犯人牧野千尋 正解
+
殺害方法正解
+
動機惜しい
+
+ +
記録
+
+
解決タイム08:41
+
質問回数9回
+
発見した証拠6個
+
+ +
真相
+
+
18:28黒田が水野に、記録に残さない現金取引を持ちかける
+
18:37水野がすり替えに気づき、牧野を問い詰める
+
18:50牧野が店の奥で水野を襲う
+
19:08牧野が郵便窓口で小包を発送する
+
+
+
+
+ + +
+
+ 七 設定 + 物語の外にある唯一の画面なので、ここだけは何も演出しません。時刻軸も引きません—— + ターン数や往復は回数であって盤面の時刻ではなく、同じ帯で描けば軸は「なんとなく量を表す装置」に落ちます。 + 数字を等幅にしないのも同じ理由(決めごと三)。 + 提供元を選ぶまでモデルは触れず、枠を地に沈めて押せないことを示します。既定のままなら送信にも載らないので、 + サーバは配備時の設定のまま動きます。 + 三つの数字は掛け算で効くので、積とモデルの呼び出し回数を文で返します。ここを出さないと、 + 3 と入れた値が黙って 2 に削られる理由(clampLimits)が誰にも分かりません。 +
+
+
+
+
← 事件を選ぶ
+

設定

+

この端末にだけ保存されます。サーバには残らず、あなたのプレイにだけ効きます。

+
+ +
+
使うモデル
+ +
+
+
会話
+
NPCの受け答えと、探偵が組み立てる質問
+
+
+
+ 提供元 + Anthropic +
+
+ モデル + Claude Sonnet 5 +
+
+
+ +
+
+
判定
+
証拠の開示と、推理の採点
+
+
+
+ 提供元 + 既定のまま +
+
+ モデル + 既定のまま +
+
+
+ +

Google は APIキーが未設定のため選べません。

+
+ +
+
進行
+

新しく始める事件から効きます。進行中のものは変わりません。

+ +
+
+ ターン数 + 5 +
+
+ 1ターンの質問 + 1 +
+
+ 1話題の往復 + 3 +
+
+ +

+ 5ターン × 1問 = 全部で5問(上限20問)。
+ 1話題ごとに、モデルを7回呼びます。 +

+
+
+
+
+ +
+
+ +
+
動き
+

十の動きと、その役目

+

+ 動きは状態が変わったことを言うためにだけ使います。雰囲気のために足さない。 + 合図の類は 0.4 秒前後で終わり、長く動くのは読ませるもの(記録のせり上げ・タイプ送り)だけです。 + 動きが苦手な人には出さず、そのとき見えるのは動いた後の姿——切っても中身が消えないように、要素は完成形で書いてあります。 + 各枠の「もう一度」で繰り返せます。 +

+
+ +
+
一 目盛りが立つ
+
+
+ + + + + 19:08 +
+
+

供述から時刻が確定した瞬間。軸の下から伸び上がり、遅れて時刻が出ます。この作品でいちばん大事な出来事なので、強めに動くのはここだけ。

+
+ +
+
二 疑問
+
+
+ + + + +
+
+

辻褄の合わない供述。目盛りは立とうとして、立ちきらずに淡いまま残ります。question mark は出しますが、色は容疑者の顔料のまま——画面が先に誰かを疑わないために。

+
+ +
+
三 ひらめき
+
+
+ + + + + + +
+
+

離れた二つの証言が噛み合わないと分かった瞬間。二本の柱のあいだに線が引かれ、引き終わってから両端の目盛りが一度だけ伸びます。光らせず、繋ぐ。 + 目盛りは時刻の軸に垂直に立つので、伸びる向きは軸の向きで決まります。時刻が縦に流れるこの表では横(pin-lift-x)、 + 横に流れるレールでは縦(pin-lift)。伸ばすのは短い目盛りのほうで、在所そのものの帯ではありません + ——帯は在所の長さを持つので、四十分の区間なら四百px を超えます。

+
+ +
+
四 新事実発見
+
+
+ 窓口の受付は午後七時八分でした。レシートも残っています。 + + 新事実 + 牧野は午後六時三十五分に店を出たと述べた + +
+
+

記録は聞き込み中に画面上にないので、増えたことを帯で被せて知らせます。箱にはせず、二本の罫線と薄い覆いだけ。 + 操作は塞がず 2.6 秒で引きます。祝いはしません——増えたのは事実であって手柄ではないので。

+
+ +
+
五 発話が続く
+
+
+ 牧野千尋 + 午後六時三十五分には店を出ています。 + 窓口の受付は午後七時八分でした。 + ……雨でしたから。 +
+
+

ひとりが続けて喋るとき、一行ずつ置いていきます。間は 0.5 秒。読み終わる前に次が来ると急かされるので、詰めすぎない。

+
+ +
+
六 タイプ送り
+
+

+
+

一字ずつ送る読み上げ。65 ミリ秒/字は、既にアプリで動いている TypewriterBriefing と同じ値です。

+
+ +
+
七 記録のせり上げ
+
+
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。

+
+ +
+
+

記録を下から流します。既存の briefing-crawl と同じ作りで、移動距離は本文の高さから決まるので、長い記録ほど自然に長く流れます。下端は地に溶かして、切れ目を作らない。

+
+ +
+
八 入り込む
+
+
+
+ 雨の古書店、十九時八分のレシート + + + + + + 牧野千尋 +
+ +
+
+

記録を読み終えて、その場に入る敷居。墨の中から浮かび上がり、寄りが戻って焦点が合います。 + 0.76 秒。強く動かすのはこの一度だけで、他の九つは合図に徹します。既存の screen-enter は + 1.03 倍で体感が無いので、ここに寄せた分だけ他から引きました。

+
+ +
+
九 選択肢が開く
+
+
+ 訊けそうなこと + + レシートを見せてもらえますか + 黒田さんとは話しましたか + +
+
+

畳んであるものが開くので、高さそのものを動かします。中身を透かせるだけだと、下の入力欄が飛んで見えるため。

+
+ +
+
十 判定が出る
+
+
+ 犯人牧野千尋 正解 + 殺害方法正解 + 動機惜しい +
+
+

上から順に 0.2 秒ずつ。一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になります。

+
+ +
+
+ +
+
決めごと
+

この案が守る四つ

+
+
+ +
+

強調色を持たない

+

今の琥珀は「未質問」「残り件数」「捜査メモ」「推理ボタン」「提出ボタン」の五つを同時に指していて、結果として何も指していません。 + 代わりに容疑者へ顔料を配り、明度を揃える。画面があなたより先に誰かを怪しむことがなくなります

+
+
+
+ +
+

朱は、告発の一度だけ

+

全画面を通して #D2452E が現れるのは、犯人を指す操作と、そこへ向かう入口だけ。 + 取り消せない一手だからこそ、他のどこにも同じ色を置きません。結果の外れにも使いません

+
+
+
+ +
+

等幅なら、それは盤面の時刻

+

ターン数・件数・正誤は等幅にしません。数字の形そのものが「これは時刻だ」と言う状態を作ります。 + ただし会話の中の時刻は地の文のまま。喋っている途中で書体が変わると、読みがそこで切れるので。 + 等幅は時刻軸・時間帯・解決タイムのような、システムが刻んだ時刻に限ります。

+
+
+
+ +
+

箱は増やさない

+

既存方針どおり、角丸+枠+塗りの箱は作らず罫線で区切ります。時刻軸も箱ではなく、 + 二本の罫線のあいだの帯として置く。新しく足すのは色と縦線一本だけです。

+
+
+
+
+ +
+ + diff --git a/mocks/effects/_effects.css b/mocks/effects/_effects.css new file mode 100644 index 0000000..6541896 --- /dev/null +++ b/mocks/effects/_effects.css @@ -0,0 +1,594 @@ +/* + 十の動きだけを抜き出した台。原典は direction.html の「動き」の節で、 + CSS と組みはそこから写している。片方だけ直すとカタログが二枚に割れるので、 + 値を変えるときは direction.html と src/client/index.css も一緒に直す。 +*/ +:root { + --sumi: #141317; + --sumi-2: #1c1a21; + --sumi-3: #232028; + --keisen: #302c39; + --kinari: #e9e4d9; + --nezumi: #8e8896; + --nezumi-dim: #625d6c; + + --asagi: #5e8b8c; + --asagi-t: #8ab5b5; + --fuji: #8079a6; + --fuji-t: #ada6cf; + --karashi: #b49a55; + --karashi-t: #d6bd77; + --suou: #96556a; + --suou-t: #c68698; + + --shu: #d2452e; + --byakuroku: #7fa88a; + + --mincho: "Shippori Mincho B1", "Hiragino Mincho ProN", "Yu Mincho", serif; + --gothic: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", sans-serif; + --mono: "Roboto Mono", ui-monospace, monospace; +} +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--sumi); + color: var(--kinari); + font-family: var(--gothic); + font-size: 15px; + line-height: 1.8; + -webkit-font-smoothing: antialiased; +} +/* ================= 動き ================= */ +.motions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(258px, 1fr)); + gap: 1px; + background: var(--keisen); + border: 1px solid var(--keisen); +} +.motion { + background: var(--sumi); + padding: 15px 18px 18px; + display: flex; + flex-direction: column; + gap: 10px; +} +.mhead { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} +.mnm { + font-family: var(--mincho); + font-size: 15px; + font-weight: 500; + letter-spacing: 0.05em; +} +.replay { + font-family: var(--gothic); + font-size: 10px; + letter-spacing: 0.16em; + color: var(--nezumi-dim); + background: none; + border: 1px solid var(--keisen); + padding: 3px 9px; + cursor: pointer; + flex: none; +} +.replay:hover { + color: var(--kinari); + border-color: var(--nezumi-dim); +} +.replay:focus-visible { + outline: 1px solid var(--kinari); + outline-offset: 2px; +} +/* 舞台の高さは全枚で同じに保つ。一枚でも違うと隣と揃わない。 */ +.stage { + height: 112px; + display: flex; + flex-direction: column; + justify-content: center; +} +.mwhy { + margin: 0; + font-size: 11.5px; + line-height: 1.7; + color: var(--nezumi); +} + +/* 一 目盛りが立つ */ +.d-rail { + position: relative; + height: 42px; +} +.d-rail .line { + position: absolute; + left: 0; + right: 0; + top: 28px; + height: 1px; + background: var(--keisen); +} +.d-rail .pin { + position: absolute; + top: 16px; + width: 2px; + height: 13px; + transform-origin: 50% 100%; +} +.d-rail .at { + position: absolute; + top: 0; + translate: -50%; + color: var(--asagi-t); + font-family: var(--mono); + font-variant-numeric: tabular-nums; + font-size: 11px; +} +.play .d-rail .pin.fresh { + animation: pin-rise 420ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} +.play .d-rail .at { + animation: at-in 360ms 150ms ease-out both; +} +@keyframes pin-rise { + from { + transform: scaleY(0); + } +} +@keyframes at-in { + from { + opacity: 0; + transform: translateY(5px); + } +} + +/* 五 発話が続く */ +.d-turn { + display: flex; + flex-direction: column; + gap: 5px; + padding-left: 10px; + border-left: 1px solid var(--asagi); +} +.d-turn .who { + font-size: 10px; + letter-spacing: 0.1em; + color: var(--asagi-t); +} +.d-turn .l { + font-size: 12px; + line-height: 1.7; +} +.play .d-turn .l { + animation: line-in 360ms ease-out both; +} +.play .d-turn .l:nth-child(2) { + animation-delay: 100ms; +} +.play .d-turn .l:nth-child(3) { + animation-delay: 620ms; +} +.play .d-turn .l:nth-child(4) { + animation-delay: 1140ms; +} +@keyframes line-in { + from { + opacity: 0; + transform: translateY(5px); + } +} + +/* 九 選択肢が開く */ +.d-hint { + display: flex; + flex-direction: column; + border-top: 1px solid var(--keisen); +} +.d-hint .head { + display: flex; + justify-content: space-between; + padding: 6px 0; + font-size: 11px; + color: var(--nezumi-dim); +} +.d-hint .fold { + display: grid; + grid-template-rows: 1fr; +} +.d-hint .inner { + overflow: hidden; + display: flex; + flex-direction: column; +} +.d-hint .q { + padding: 6px 0; + border-top: 1px solid var(--keisen); + font-size: 11.5px; + color: var(--nezumi); +} +.play .d-hint .fold { + animation: fold-open 360ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} +@keyframes fold-open { + from { + grid-template-rows: 0fr; + } +} + +/* 十 判定が出る */ +.d-rows { + display: flex; + flex-direction: column; + border-top: 1px solid var(--keisen); +} +.d-rows .r { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 6px 0; + border-bottom: 1px solid var(--keisen); + font-size: 12px; +} +.d-rows .k { + color: var(--nezumi); +} +.d-rows em { + font-style: normal; + color: var(--byakuroku); +} +.play .d-rows .r { + animation: row-in 400ms ease-out both; +} +.play .d-rows .r:nth-child(2) { + animation-delay: 200ms; +} +.play .d-rows .r:nth-child(3) { + animation-delay: 400ms; +} +@keyframes row-in { + from { + opacity: 0; + transform: translateY(6px); + } +} + +/* 二 疑問 — 立ちきらない目盛り */ +.d-rail .pin.hollow { + opacity: 0.34; +} +.d-rail .qm { + position: absolute; + top: 1px; + translate: -50%; + font-size: 13px; + color: var(--suou-t); +} +.play .doubt .pin.wobble { + animation: waver 900ms ease-in-out both; +} +.play .doubt .qm { + animation: at-in 360ms 260ms ease-out both; +} +@keyframes waver { + 0% { + transform: scaleY(0); + } + 45% { + transform: scaleY(1.18); + } + 72% { + transform: scaleY(0.88); + } +} + +/* + * 三 ひらめき — 噛み合わない二本のあいだに線が架かる。 + * + * 縦組みで描く。アリバイ表は時が縦に流れるので、目盛りは軸に垂直な横の短い棒になり、 + * 伸びる向きも横(pin-lift-x)。横のレールで描くと pin-lift との使い分けが絵から消える。 + */ +.d-cols { + position: relative; + height: 100%; + /* 実物の列は 140px 間隔。幅いっぱいに広げると線だけが長い十字に見え、表の比率から離れる。 */ + width: 320px; + margin: 0 auto; +} +/* 在所の柱。裏付けのあるところは実線、申告だけのところは破線。 */ +.d-cols .col { + position: absolute; + top: 8px; + bottom: 8px; + width: 3px; +} +.d-cols .col.upper { + bottom: 50%; +} +.d-cols .col.lower { + top: 50%; + width: 0; + border-left: 1px dashed currentColor; + opacity: 0.45; +} +.d-cols .col.claim { + width: 0; + border-left: 1px dashed currentColor; + opacity: 0.55; +} +/* 二本のあいだに架かる線。引き終わってから両端が動く。 */ +.d-cols .link { + position: absolute; + top: 50%; + height: 0; + border-top: 1px dashed var(--nezumi-dim); + opacity: 0.8; + transform-origin: 0 50%; +} +/* + * 両端の目盛り。外の枠が浮き、中の棒が伸びる——一つの要素に animation は一つしか + * 持てないので、浮きと伸びを親子で分ける。 + */ +.d-cols .cpin { + position: absolute; + top: 50%; + margin-left: -5.5px; +} +.d-cols .cpin i { + display: block; + width: 11px; + height: 1px; + background: currentColor; +} +.play .spark .link { + animation: draw 520ms 180ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} +/* 線を引き終わる 700ms(draw の 180ms 遅れ+520ms)を待ってから、端を置いて伸ばす。 */ +.play .spark .cpin { + animation: line-in 360ms 700ms ease-out both; +} +.play .spark .cpin i { + animation: pin-lift-x 620ms 700ms ease-out both; +} +/* 縦向き——時が横に流れる軸で使う。いま出番はなく、下の横向きと対にして + 「目盛りの向きは軸の向きで決まる」を語彙の側で示すために置いてある。 */ +@keyframes pin-lift { + 55% { + transform: scaleY(1.5); + } +} +/* 横向きの片割れ。目盛りは時刻の軸に垂直に立つので、軸が縦に流れる + アリバイ表では目盛りが横の短い棒になり、伸びる向きもこちらへ直交する。 */ +@keyframes pin-lift-x { + 55% { + transform: scaleX(1.5); + } +} +.pin-lift-x { + animation: pin-lift-x 620ms 180ms ease-out both; +} + +/* 新事実発見 — 記録は聞き込み中に画面上にないので、帯で被せて知らせる。 + 箱は作らず、二本の罫線と薄い覆いだけ。操作は塞がず、2.6 秒で引く。 */ +.d-over { + position: relative; + height: 112px; + display: flex; + align-items: center; + overflow: hidden; +} +.d-over .bg { + font-size: 12px; + line-height: 1.9; + color: var(--nezumi-dim); +} +.d-over .band { + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + gap: 3px; + padding: 9px 0; + border-top: 1px solid var(--asagi); + border-bottom: 1px solid var(--asagi); + background: rgba(20, 19, 23, 0.93); +} +.d-over .k { + font-family: var(--mono); + font-size: 9.5px; + letter-spacing: 0.24em; + color: var(--asagi-t); +} +.d-over .v { + font-size: 12px; + line-height: 1.6; +} +.play .d-over .band { + animation: band 2600ms both; +} +@keyframes band { + 0% { + opacity: 0; + transform: translateY(-50%) scaleY(0.86); + } + 11% { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + 84% { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + 100% { + opacity: 0; + transform: translateY(-50%) scaleY(1); + } +} +@keyframes draw { + from { + transform: scaleX(0); + } +} + +/* プロローグのせり上げ — 既存 briefing-crawl と同じ考え方 */ +.d-crawl { + position: relative; + height: 112px; + overflow: hidden; +} +.d-crawl .inner { + font-family: var(--mincho); + font-size: 12px; + line-height: 2.2; + letter-spacing: 0.04em; +} +.d-crawl .inner p { + margin: 0 0 14px; +} +.d-crawl .veil { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 34px; + background: linear-gradient(to bottom, transparent, var(--sumi)); +} +.play .d-crawl .inner { + animation: crawl 9s linear both; +} +@keyframes crawl { + from { + transform: translateY(112px); + } + to { + transform: translateY(-100%); + } +} + +/* タイプ送り — アプリ実装と同じ 65ms/字 */ +.d-type { + margin: 0; + font-family: var(--mincho); + font-size: 12.5px; + line-height: 2; + letter-spacing: 0.04em; +} +.play .d-type span { + animation: type-in 1ms both; + animation-delay: calc(var(--i) * 65ms); +} +@keyframes type-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 入り込む — 敷居を越える一度だけ。墨の中から浮かび上がり、焦点が合う。 + 既存 screen-enter は 1.03 倍でほぼ体感できないので、ここだけ強く寄せる。 */ +.d-dive { + position: relative; + height: 112px; + overflow: hidden; + display: flex; + align-items: center; +} +.d-dive .inner { + display: flex; + flex-direction: column; + gap: 13px; + width: 100%; +} +.d-dive .ttl { + font-family: var(--mincho); + font-size: 15px; + letter-spacing: 0.05em; +} +.d-dive .bar { + position: relative; + height: 1px; + background: var(--keisen); +} +.d-dive .bar i { + position: absolute; + top: -6px; + width: 2px; + height: 13px; +} +.d-dive .who { + font-size: 12px; + color: var(--asagi-t); +} +.d-dive .veil { + position: absolute; + inset: 0; + background: var(--sumi); + opacity: 0; +} +.play .d-dive .inner { + animation: dive 760ms cubic-bezier(0.16, 1, 0.3, 1) both; +} +.play .d-dive .veil { + animation: unveil 560ms ease-out both; +} +@keyframes dive { + from { + opacity: 0; + transform: scale(1.16); + filter: blur(8px); + } +} +@keyframes unveil { + from { + opacity: 1; + } +} + +/* 動きが苦手な人には出さない。演出のために体調を崩させる理由はない。 + 要素は動いた後の姿で書いてあるので、切っても完成形が残る。 */ +@media (prefers-reduced-motion: reduce) { + .play .d-rail .pin.fresh, + .play .d-rail .at, + .play .d-turn .l, + .play .d-hint .fold, + .play .d-rows .r, + .play .doubt .pin.wobble, + .play .doubt .qm, + .play .spark .link, + .play .spark .cpin, + .play .spark .cpin i, + .play .d-over .band, + .play .d-crawl .inner, + .play .d-type span, + .play .d-dive .inner, + .play .d-dive .veil { + animation: none; + } +} + +/* + * 一枚に一つずつ載せる頁。枠の外側に余白を置かないのは、この頁をそのまま + * ストーリーボードの升目へ嵌めるため。余白は嵌めた側が持つ。 + * + * .motions より後ろに置く。前に置くと、あとから来る低い詳細度の .motions が + * 勝ってしまい、升目の指定が効かない。 + */ +.sheet.one { + padding: 0; +} + +.sheet.one .motions { + grid-template-columns: 1fr; + border: 0; + background: none; +} diff --git a/mocks/effects/_effects.js b/mocks/effects/_effects.js new file mode 100644 index 0000000..1393c97 --- /dev/null +++ b/mocks/effects/_effects.js @@ -0,0 +1,32 @@ +// タイプ送りは一字ずつ遅らせる。順番だけ CSS に渡して、時間の決定は CSS 側に残す。 +for (const el of document.querySelectorAll('[data-type]')) { + const chars = [...el.dataset.type] + el.replaceChildren( + ...chars.map((ch, i) => { + const s = document.createElement('span') + s.textContent = ch + s.style.setProperty('--i', String(i)) + return s + }), + ) +} + +for (const m of document.querySelectorAll('.motion')) { + // クラスを外して強制的に再計算させないと、同じアニメーションは再生し直されない。 + const play = () => { + m.classList.remove('play') + void m.offsetWidth + m.classList.add('play') + } + m.querySelector('.replay').addEventListener('click', play) + new IntersectionObserver( + (entries, obs) => { + for (const e of entries) + if (e.isIntersecting) { + play() + obs.unobserve(e.target) + } + }, + { threshold: 0.55 }, + ).observe(m) +} diff --git a/mocks/effects/band.html b/mocks/effects/band.html new file mode 100644 index 0000000..85d05d9 --- /dev/null +++ b/mocks/effects/band.html @@ -0,0 +1,32 @@ + + + + +AlibAI — 四 新事実発見 + + + + + + +
+
+
+
四 新事実発見
+
+
+ 窓口の受付は午後七時八分でした。レシートも残っています。 + + 新事実 + 牧野は午後六時三十五分に店を出たと述べた + +
+
+

記録は聞き込み中に画面上にないので、増えたことを帯で被せて知らせます。箱にはせず、二本の罫線と薄い覆いだけ。 + 操作は塞がず 2.6 秒で引きます。祝いはしません——増えたのは事実であって手柄ではないので。

+
+
+
+ + + diff --git a/mocks/effects/crawl.html b/mocks/effects/crawl.html new file mode 100644 index 0000000..912a0fe --- /dev/null +++ b/mocks/effects/crawl.html @@ -0,0 +1,31 @@ + + + + +AlibAI — 七 記録のせり上げ + + + + + + +
+
+
+
七 記録のせり上げ
+
+
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。

+
+ +
+
+

記録を下から流します。既存の briefing-crawl と同じ作りで、移動距離は本文の高さから決まるので、長い記録ほど自然に長く流れます。下端は地に溶かして、切れ目を作らない。

+
+
+
+ + + diff --git a/mocks/effects/dive.html b/mocks/effects/dive.html new file mode 100644 index 0000000..966a544 --- /dev/null +++ b/mocks/effects/dive.html @@ -0,0 +1,38 @@ + + + + +AlibAI — 八 入り込む + + + + + + +
+
+
+
八 入り込む
+
+
+
+ 雨の古書店、十九時八分のレシート + + + + + + 牧野千尋 +
+ +
+
+

記録を読み終えて、その場に入る敷居。墨の中から浮かび上がり、寄りが戻って焦点が合います。 + 0.76 秒。強く動かすのはこの一度だけで、他の九つは合図に徹します。既存の screen-enter は + 1.03 倍で体感が無いので、ここに寄せた分だけ他から引きました。

+
+
+
+ + + diff --git a/mocks/effects/doubt.html b/mocks/effects/doubt.html new file mode 100644 index 0000000..8ac4871 --- /dev/null +++ b/mocks/effects/doubt.html @@ -0,0 +1,30 @@ + + + + +AlibAI — 二 疑問 + + + + + + +
+
+
+
二 疑問
+
+
+ + + + +
+
+

辻褄の合わない供述。目盛りは立とうとして、立ちきらずに淡いまま残ります。question mark は出しますが、色は容疑者の顔料のまま——画面が先に誰かを疑わないために。

+
+
+
+ + + diff --git a/mocks/effects/fold.html b/mocks/effects/fold.html new file mode 100644 index 0000000..774b258 --- /dev/null +++ b/mocks/effects/fold.html @@ -0,0 +1,31 @@ + + + + +AlibAI — 九 選択肢が開く + + + + + + +
+
+
+
九 選択肢が開く
+
+
+ 訊けそうなこと + + レシートを見せてもらえますか + 黒田さんとは話しましたか + +
+
+

畳んであるものが開くので、高さそのものを動かします。中身を透かせるだけだと、下の入力欄が飛んで見えるため。

+
+
+
+ + + diff --git a/mocks/effects/line-in.html b/mocks/effects/line-in.html new file mode 100644 index 0000000..5fb55e1 --- /dev/null +++ b/mocks/effects/line-in.html @@ -0,0 +1,30 @@ + + + + +AlibAI — 五 発話が続く + + + + + + +
+
+
+
五 発話が続く
+
+
+ 牧野千尋 + 午後六時三十五分には店を出ています。 + 窓口の受付は午後七時八分でした。 + ……雨でしたから。 +
+
+

ひとりが続けて喋るとき、一行ずつ置いていきます。間は 0.5 秒。読み終わる前に次が来ると急かされるので、詰めすぎない。

+
+
+
+ + + diff --git a/mocks/effects/pin-rise.html b/mocks/effects/pin-rise.html new file mode 100644 index 0000000..0af8a3b --- /dev/null +++ b/mocks/effects/pin-rise.html @@ -0,0 +1,31 @@ + + + + +AlibAI — 一 目盛りが立つ + + + + + + +
+
+
+
一 目盛りが立つ
+
+
+ + + + + 19:08 +
+
+

供述から時刻が確定した瞬間。軸の下から伸び上がり、遅れて時刻が出ます。この作品でいちばん大事な出来事なので、強めに動くのはここだけ。

+
+
+
+ + + diff --git a/mocks/effects/rows.html b/mocks/effects/rows.html new file mode 100644 index 0000000..836bb87 --- /dev/null +++ b/mocks/effects/rows.html @@ -0,0 +1,29 @@ + + + + +AlibAI — 十 判定が出る + + + + + + +
+
+
+
十 判定が出る
+
+
+ 犯人牧野千尋 正解 + 殺害方法正解 + 動機惜しい +
+
+

上から順に 0.2 秒ずつ。一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になります。

+
+
+
+ + + diff --git a/mocks/effects/spark.html b/mocks/effects/spark.html new file mode 100644 index 0000000..270ec92 --- /dev/null +++ b/mocks/effects/spark.html @@ -0,0 +1,32 @@ + + + + +AlibAI — 三 ひらめき + + + + + + +
+
+
+
三 ひらめき
+
+
+ + + + + + +
+
+

離れた二つの証言が噛み合わないと分かった瞬間。二本の柱のあいだに線が引かれ、引き終わってから両端の目盛りが一度だけ伸びます。光らせず、繋ぐ。アリバイ表は縦に時が流れるので目盛りは横棒——伸ばすのは pin-lift-x、横のレールで使う pin-lift の向きだけ直交した片割れです。

+
+
+
+ + + diff --git a/mocks/effects/typing.html b/mocks/effects/typing.html new file mode 100644 index 0000000..26599da --- /dev/null +++ b/mocks/effects/typing.html @@ -0,0 +1,25 @@ + + + + +AlibAI — 六 タイプ送り + + + + + + +
+
+
+
六 タイプ送り
+
+

+
+

一字ずつ送る読み上げ。65 ミリ秒/字は、既にアプリで動いている TypewriterBriefing と同じ値です。

+
+
+
+ + + diff --git a/mocks/mobile/_phone.css b/mocks/mobile/_phone.css index f0afb98..5d02f87 100644 --- a/mocks/mobile/_phone.css +++ b/mocks/mobile/_phone.css @@ -17,6 +17,8 @@ --kinari: #e9e4d9; --nezumi: #8e8896; --nezumi-dim: #625d6c; + /* 場所は人物の色を持たない。他の色と同じ口から引けるよう、明るい側だけ揃えておく。 */ + --nezumi-fg: #aaa4b2; --asagi: #5e8b8c; --asagi-fg: #8ab5b5; @@ -315,6 +317,15 @@ textarea { .face.on { box-shadow: 0 0 0 1.5px currentColor; } +/* + 場所の印。人の丸に対して角を立て、塗らずに罫線で囲う。 + 名簿では人と場所が同じ列に並ぶので、色を抜いただけでは「暗い人物」に見える。 + 塗った四角にすると今度は箱になるので、線だけで区画を示す。 +*/ +.face.pl { + border-radius: 2px; + border: 1px solid var(--keisen); +} .int-head { padding: 10px 12px; } @@ -336,23 +347,55 @@ textarea { font-weight: 500; margin-top: 4px; } +/* + 相手を替える口。名前と同じ行の右端へ小さく並べる。 + 端末には表の列見出しが無いので、切り替えられる場所はここだけになる。 +*/ +.int-head .row2 { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; +} +.int-head .row2 .swap { + display: flex; + flex: none; + gap: 10px; + font-size: 10.5px; + color: var(--nezumi-dim); +} .int-head .pers { font-size: 10.5px; color: var(--nezumi-dim); } -/* 時刻軸(帯)— この案の主役 */ +/* + * 時刻軸(帯)— この案の主役。 + * + * 深さは三段ぶん要る。上から、両端の時刻/印の道/刻限の一段。 + * 元は 34px の一段しか無く、両端の時刻と印が同じ高さの帯を奪い合っていた。白紙のうちは + * 気づかないが、18:23 のように端寄りの時刻が確定した途端、印が「18:20」の字を貫く。 + * さらに刻限の窓は札を連れてくるので、印の道の下にもう一段いる。 + */ .rail { position: relative; - height: 34px; + height: 64px; padding: 0 10px; display: flex; align-items: center; + + /* 刻限の印の深さ。形は下の共有の塊が持ち、どこに置くかだけ器が決める。 */ + --dl-top: 20px; + --dl-bottom: 26px; + --dl-win: 42px; + --dl-lb: 47px; } +/* 線も明示的に置く。flex の中央任せだと、帯を深くした途端に印とずれる。 */ .rail .line { position: absolute; left: 10px; right: 10px; + top: 28px; height: 1px; background: var(--keisen); } @@ -360,31 +403,125 @@ textarea { position: absolute; width: 2px; height: 13px; - top: 11px; + top: 22px; } .rail .pin.hollow { opacity: 0.32; } .rail .now { position: absolute; - top: 6px; + top: 18px; width: 1px; - height: 23px; + height: 20px; background: var(--kinari); } .rail .cap { position: absolute; left: 10px; - top: 1px; + top: 0; } .rail .cap2 { position: absolute; right: 10px; - top: 1px; + top: 0; +} + +/* + * ---- 刻限の印(docs/design/deadline-window.md)。聞き込みの帯と告発の拡大軸で共に使う ---- + * + * 机は時刻が上から下へ流れるので横一本の線になるが、端末は左から右なので縦の目盛りになる。 + * 一点を指す印(遺体発見・確定・第三者の見立て)は帯を貫き、幅を持つ印(範囲・不明)は + * 両端に返しの付いた線を印の道の下へ渡す。塗らないのは机と同じ理由で、面を足すと容疑者の帯と競う。 + */ +.rail .deadline, +.rail-big .deadline { + position: absolute; + top: var(--dl-top); + bottom: var(--dl-bottom); + width: 1px; + background: var(--nezumi-dim); +} +/* 第三者がそう言っているだけの刻限。裏が取れていないので点線にする。 */ +.rail .deadline.claimed, +.rail-big .deadline.claimed { + background: none; + width: 0; + border-left: 1px dotted var(--nezumi-dim); +} +.rail .window, +.rail-big .window { + position: absolute; + top: var(--dl-win); + height: 1px; + background: var(--nezumi-dim); +} +/* 両端の返し。これが無いと、ただの罫線に見えて幅を指していることが伝わらない。 */ +.rail .window::before, +.rail .window::after, +.rail-big .window::before, +.rail-big .window::after { + content: ""; + position: absolute; + top: -3px; + width: 1px; + height: 7px; + background: var(--nezumi-dim); +} +.rail .window::before, +.rail-big .window::before { + left: 0; +} +.rail .window::after, +.rail-big .window::after { + right: 0; +} +/* まだ何も無い状態。芯を点線にして、時刻の代わりに ? を置く。 */ +.rail .window.unknown, +.rail-big .window.unknown { + background: none; + height: 0; + border-top: 1px dotted var(--nezumi-dim); +} +/* + * 刻限の札。印の子ではなく兄弟にしてあるので、深さは器の変数から取る。 + * 既定は印を跨いで中央。端に寄った印(遺体発見は軸の 92% に立つ)では、 + * 札の右端を印に合わせて内側へ折り返す——390px しかないので、はみ出せば読めなくなる。 + */ +.rail .lb, +.rail-big .lb { + position: absolute; + top: var(--dl-lb); + transform: translateX(-50%); + white-space: nowrap; + font-family: var(--mincho); + font-size: 9.5px; + line-height: 1.4; + letter-spacing: 0.06em; + color: var(--nezumi); +} +.rail .lb.end, +.rail-big .lb.end { + transform: translateX(-100%); +} +.rail .lb.start, +.rail-big .lb.start { + transform: none; +} +.rail .lb .t, +.rail-big .lb .t { + font-size: 9px; + color: var(--nezumi-dim); +} +/* 誰の見立てかは、机では線の下の一行。端末にはその一段が無いので札の尾に続ける。 */ +.rail .lb .by, +.rail-big .lb .by { + margin-left: 6px; } /* 最新の発話を下端に置く。交互とは限らないので、上は溢れるに任せる。 */ .log { + /* 新事実の帯を会話へ被せるための基準。位置取りだけで、並びは変わらない。 */ + position: relative; flex: 1; overflow: hidden; padding: 12px; @@ -394,6 +531,54 @@ textarea { gap: 15px; } +/* + * 新事実の帯。聞き込みのあいだ記録は画面上にないので、増えたことは被せて知らせる。 + * 箱は作らず、二本の罫線と薄い覆いだけ。操作を塞がないので pointer-events は殺す。 + */ +.newfact { + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + gap: 3px; + padding: 9px 12px; + border-top: 1px solid var(--asagi); + border-bottom: 1px solid var(--asagi); + /* + 透かさない。端末は幅が狭く帯が会話の行を丸ごと覆うので、わずかでも透けると + 後ろの字が帯の字と重なって、どちらも読めなくなる。 + */ + background: var(--sumi); + pointer-events: none; +} +.newfact .k { + font-family: var(--mono); + font-size: 9.5px; + letter-spacing: 0.24em; + color: var(--asagi-fg); +} +.newfact .v { + font-size: 12px; + line-height: 1.6; +} + +/* + * 訊けそうなことの開閉。高さそのものを動かすので、中身は grid の行に入れる。 + * 透かせるだけだと、下の入力欄が動かないまま文字だけ現れて飛んで見える。 + */ +.hint .fold { + display: grid; + grid-template-rows: 1fr; +} +.hint .fold .inner { + overflow: hidden; + display: flex; + flex-direction: column; +} + /* 発話の塊。同じ人が続けて喋るあいだ、名前は一度きりしか出さない。 塊の左に立つ縦罫がその人の顔料で、どこまでが一人の言葉かを示す。 */ .turn { @@ -438,6 +623,10 @@ textarea { flex-direction: column; border-top: 1px solid var(--keisen); } +/* 台本を使い切ったら畳みごと下ろす。display を持つので hidden だけでは消えない。 */ +.hint[hidden] { + display: none; +} .hint .head { display: flex; justify-content: space-between; @@ -486,11 +675,20 @@ textarea { color: var(--nezumi); margin: 0 0 18px; } -/* 拡大した時刻軸。ここでは目盛りを掴んで動かす */ +/* + * 拡大した時刻軸。ここでは目盛りを掴んで動かす。 + * 段の下に刻限の一段(窓の線と札)を挟むので、帯そのものより 30px ぶん深い。 + */ .rail-big { position: relative; - height: 106px; + height: 136px; margin-bottom: 14px; + + /* 刻限の印の深さ。三段の下、軸の上に置く。 */ + --dl-top: 4px; + --dl-bottom: 48px; + --dl-win: 88px; + --dl-lb: 94px; } .rail-big .lane { position: absolute; @@ -521,10 +719,15 @@ textarea { display: flex; justify-content: space-between; } +/* + * 掴んで動かす目盛り。段の下で止める——指した時刻が刻限と同じところに落ちたとき + * (このモックの既定がまさにそれ)、朱の線が刻限の印を丸ごと隠してしまうため。 + * 段を出たところから下は刻限の側の受け持ちで、実線か点線かはそこで見分ける。 + */ .rail-big .mark { position: absolute; top: -4px; - bottom: 18px; + bottom: 57px; width: 1.5px; background: var(--shu); } diff --git a/mocks/mobile/accusation.html b/mocks/mobile/accusation.html index 9efa3fa..0e97de5 100644 --- a/mocks/mobile/accusation.html +++ b/mocks/mobile/accusation.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
← 聞き込みに戻る @@ -57,9 +58,20 @@

犯人を指し示す

var filled = Mock.str('filled', '0') === '1' var pick = Mock.str('who', C.truth.culprit) + /* + 掴んで動かす目盛りは「プレイヤーが指した時刻」で、盤面が知っている刻限ではない。 + 既定は真相の一手(犯行の時刻)に置く——このモックは指名も真相に合わせてあるので、 + 書き上がった告発の絵として辻褄が合う。刻限のほうは #death= が別に描く。 + */ + var keyBeat = C.truth.timeline.filter(function (t) { + return t.key + })[0] + document.getElementById('railBig').innerHTML = Mock.railBig({ segments: st.segments, - at: C.deadline.at, + at: keyBeat.at, + // 刻限の状態は #death= で切り替える(docs/design/deadline-window.md)。 + death: Mock.str('death', 'unknown'), }) document.getElementById('pick').innerHTML = C.cast diff --git a/mocks/mobile/briefing.html b/mocks/mobile/briefing.html index 6333497..c0a43f8 100644 --- a/mocks/mobile/briefing.html +++ b/mocks/mobile/briefing.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
@@ -41,15 +42,31 @@ ;(function () { var C = Mock.case document.getElementById('no').textContent = '記録 ' + C.no - Mock.readOut( - document.getElementById('crawl'), - C.brief - .map(function (p) { - return '

' + p + '

' - }) - .join(''), - { scroller: document.querySelector('.p-brief') }, - ) + + var crawl = document.getElementById('crawl') + var body = C.brief + .map(function (p) { + return '

' + p + '

' + }) + .join('') + + /* + 記録の見せ方は二通り。既定は 六 タイプ送りで、読む速さをプレイヤーが握る。 + #brief=crawl にすると 七 記録のせり上げ——全文が下から流れる、映画の導入。 + 実装も同じ二択を設定で切り替える(src/client/lib/briefing-mode.ts)。 + */ + if (Mock.params.brief === 'crawl') { + crawl.innerHTML = body + crawl.classList.add('briefing-crawl') + /* + 尺は実装だと本文の長さから決まる。ここは静止画としても見る枚なので、 + 流れ切って空の画面が残らないよう、長めに取って途中で写るようにしてある。 + */ + crawl.style.animationDuration = '60s' + } else { + Mock.readOut(crawl, body, { scroller: document.querySelector('.p-brief') }) + } + Mock.ready() })() diff --git a/mocks/mobile/case-overview.html b/mocks/mobile/case-overview.html index 5de7634..696bb70 100644 --- a/mocks/mobile/case-overview.html +++ b/mocks/mobile/case-overview.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
← 事件を選ぶ @@ -49,6 +50,15 @@

+ + +
事件の記録をもう一度読む @@ -68,40 +78,69 @@

document.getElementById('to').textContent = C.span.to document.getElementById('railLead').textContent = C.railLead + /* + 名簿の一行。人物・遺体・場所を同じ組みで並べる。選ぶという一手は同じで、 + 押した先ですることだけが違う。 + */ + var row = function (p, kind) { + /* + 所見も死因も無い事件では、遺体の行は押せないまま。 + 押せるのに何も出ない行を名簿に作らない。 + */ + var canPick = kind !== 'victim' || C.victim.investigable + var on = pick === p.key + var hue = 'var(--' + p.hue + Mock.lit + ')' + // 右端の札。喋らない相手にだけ出して、聞き込みではないことを名簿の上で示す。 + var tag = kind === 'cast' ? '' : canPick ? '調べる' : '被害者' + return ( + '' + ) + } + document.getElementById('cast').innerHTML = C.cast - .concat([C.victim]) .map(function (p) { - var isVictim = p.key === C.victim.key - return ( - '' - ) + return row(p, 'cast') }) + .concat([row(C.victim, 'victim')]) .join('') + // 場所の無い事件では見出しごと出さない。空の見出しは「何か足りない」に見える。 + if (Mock.places.length > 0) { + document.getElementById('placeGrp').hidden = false + document.getElementById('places').innerHTML = Mock.places + .map(function (p) { + return row(p, 'place') + }) + .join('') + } + var who = Mock.cast[pick] var go = document.getElementById('go') - go.textContent = who.name + 'に聞き込みをする' + // 喋らない相手には「聞き込み」をしない。同じ一手でも、することが違う。 + go.textContent = who.name + (Mock.examines(pick) ? 'を調べる' : 'に聞き込みをする') go.href = './interrogation.html#turn=1&who=' + pick Array.prototype.forEach.call(document.querySelectorAll('[data-who]'), function (el) { diff --git a/mocks/mobile/detective.html b/mocks/mobile/detective.html index e058142..6c1b2ee 100644 --- a/mocks/mobile/detective.html +++ b/mocks/mobile/detective.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
diff --git a/mocks/mobile/interrogation.html b/mocks/mobile/interrogation.html index 8c9a35d..55910a0 100644 --- a/mocks/mobile/interrogation.html +++ b/mocks/mobile/interrogation.html @@ -11,33 +11,26 @@ rel="stylesheet" /> + -
+ +
@@ -45,14 +38,14 @@
- 訊けそうなこと +
- 何について訊く? - 訊く + 何について訊く? + 訊く
@@ -62,7 +55,7 @@ + + diff --git a/mocks/storyboard.html b/mocks/storyboard.html index 0e3969d..a790680 100644 --- a/mocks/storyboard.html +++ b/mocks/storyboard.html @@ -87,6 +87,14 @@ .paper.desk iframe { width: 1440px; height: 900px; } .paper.phone { width: 195px; height: 422px; } .paper.phone iframe { width: 390px; height: 844px; } + /* + 動きは一つずつ別の枚に分けてあり、ここでは升目に嵌めて並べる。 + 縮めないのは、画面の見え方ではなく速さと大きさを見る枠だから—— + 50% にすると 5px の浮きが 2.5px になり、比べたいものが消える。 + */ + .fxgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1px; background: var(--keisen); border: 1px solid var(--keisen); } + .fxgrid .paper { width: auto; height: 300px; border: 0; } + .fxgrid iframe { width: 100%; height: 300px; transform: none; } @@ -303,6 +311,32 @@

ALI_SET設定

+ +
+

ALI_FX動き

+

+ 画面をまたいで使う十の動き。枠の中で もう一度 を押すと再生する。 + 実装は src/client/index.css のキーフレームと Storybook の + Parts/十の動き にあり、この升目は mocks/effects/*.html を一枚ずつ読んでいる。 + クラス名はモックと実装で同じにしてあるので、ここで当てた場所がそのまま実装で当てる場所になる + (モック側の語彙は mocks/_motion.css)。 +

+
+
十の動きと、その役目
+
+
+
+
+
+
+
+
+
+
+
+
+
+