// casegen/accusation.js — симулятор досяжності та перевірка звинувачення const RT = require('./runtime'); function solve(cs) { const E = cs.evidence; const st = RT.newState(); let changed = true; while (changed) { changed = false; for (const cid of st.pointed) if (!st.warrants.includes(cid)) { st.warrants.push(cid); changed = true; } for (const ev of Object.values(E)) { if (st.found.includes(ev.id) || !RT.available(cs, st, ev, { ignorePuzzles: true })) continue; RT.applyFound(cs, st, ev); changed = true; } } const has = pred => Object.values(E).some(ev => st.found.includes(ev.id) && pred(ev)); return { reached: new Set(st.found), identity: st.identity, killerConfirmed: has(ev => ev.gives.confirms), methodKnown: has(ev => ev.gives.method), motiveProven: has(ev => ev.gives.motive), herringDisproved: has(ev => ev.gives.disproves === cs.herring), herringLeadFound: st.found.includes('her_lead'), lairProven: has(ev => ev.gives.lair), finalCandidates: RT.candidates(cs, st) }; } function checkAccusation(cs, ans, pinned) { const E = cs.evidence, killer = cs.citizens.find(c => c.id === cs.finale.killer); const has = pred => pinned.some(id => E[id] && pred(E[id])); const hasConfirmFor = (name) => pinned.some(id => { const ev = E[id]; if (!ev || !ev.gives || !ev.gives.confirms) return false; if (ev.gives.pointsTo) { const target = cs.citizens.find(c => c.id === ev.gives.pointsTo); return target && target.name.toLowerCase() === name; } return true; }); const accused = (ans.who || '').trim().toLowerCase(); if (accused !== killer.name.toLowerCase()) return { ok: false, hint: 'Ім\'я не сходиться з доказами: сліди з місця злочину (взуття, тканина, зріст, ДНК) звужують коло — і хтось із перевірених має підтверджене алібі.' }; if (cs.caseType === 'kidnap') { if (ans.how !== cs.finale.where) return { ok: false, hint: 'Місце не сходиться. ДЕ саме вказують знахідки: бирка ключа, план, чек за замок?' }; if (ans.why !== cs.finale.motive) return { ok: false, hint: 'Мотив не той. ЩО пов\'язувало викрадача зі зниклою особою?' }; if (!hasConfirmFor(accused)) return { ok: false, hint: 'Особу викрадача не підтверджено: потрібен збіг ДНК або спростоване алібі на дошці.' }; if (!has(ev => ev.gives.motive)) return { ok: false, hint: 'Мотив не підкріплено жодним закріпленим доказом.' }; if (!has(ev => ev.gives.lair)) return { ok: false, hint: 'Місце утримання не підкріплено закріпленим доказом — штурм наосліп ніхто не санкціонує.' }; return { ok: true }; } if (ans.how !== cs.finale.method) return { ok: false, hint: 'Спосіб суперечить висновку розтину. Перечитайте, що саме сказав патологоанатом.' }; if (ans.why !== cs.finale.motive) return { ok: false, hint: 'Мотив не той. ЩО пов\'язувало цих двох людей за тиждень до смерті?' }; if (!hasConfirmFor(accused)) return { ok: false, hint: 'Немає прямого підтвердження особи: потрібен збіг ДНК, повний відбиток зі знаряддя або спростоване алібі на дошці.' }; if (!has(ev => ev.gives.motive)) return { ok: false, hint: 'Мотив не підкріплено жодним закріпленим доказом.' }; if (!has(ev => ev.gives.method)) return { ok: false, hint: 'Спосіб смерті не підкріплено: закріпіть висновок розтину.' }; return { ok: true }; } module.exports = { solve, checkAccusation };