70 lines
4.1 KiB
JavaScript
70 lines
4.1 KiB
JavaScript
// 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 };
|