// Перевірки розділу 2: fair-play кейса, логіка актів, логіка міні-ігор. node test.js const assert = require('assert'); const { CASE, checkAccusation } = require('./case2.js'); const { MG_SYMBOLS, seqGen, seqPrefixOk, triPos, inZone } = require('./minigames.js'); // ---------- 1. Кожен доказ має рівно одне джерело, ролі збігаються ---------- const sources = {}; for (const l of CASE.locations) for (const sp of l.spots) if (sp.ev) (sources[sp.ev] ??= []).push('spot:' + sp.id); for (const s of CASE.systems) for (const q of s.queries) if (q.ev) (sources[q.ev] ??= []).push('query:' + q.id); for (const p of Object.values(CASE.people)) for (const t of p.topics) if (t.gives) (sources[t.gives] ??= []).push('topic:' + t.id); for (const id of Object.keys(CASE.evidence)) assert(sources[id]?.length === 1, `доказ "${id}" має ${sources[id]?.length || 0} джерел`); for (const id of Object.keys(sources)) assert(CASE.evidence[id], `джерело посилається на неіснуючий доказ "${id}"`); for (const l of CASE.locations) for (const sp of l.spots) if (sp.ev) assert(CASE.evidence[sp.ev].role === 'field'); for (const s of CASE.systems) for (const q of s.queries) if (q.ev) assert(CASE.evidence[q.ev].role === 'analyst'); // ---------- 2. requires існують ---------- for (const ev of Object.values(CASE.evidence)) for (const r of ev.requires || []) assert(CASE.evidence[r], `bad requires ${r}`); for (const p of Object.values(CASE.people)) for (const t of p.topics) for (const r of t.requires || []) assert(CASE.evidence[r], `bad requires ${r}`); for (const id of CASE.actGate) assert(CASE.evidence[id], `actGate: неіснуючий доказ ${id}`); // ---------- 3. Симуляція проходження з актами ---------- // Людей можна допитати лише в їхній локації — акт локації гейтить і допит. const personAct = {}; for (const l of CASE.locations) for (const pid of l.people) personAct[pid] = Math.min(personAct[pid] || 9, l.act || 1); for (const pid of Object.keys(CASE.people)) assert(personAct[pid], `персонаж "${pid}" не стоїть у жодній локації`); function simulate(skipIds = new Set()) { const pinned = new Set(); let act = 1, progress = true; while (progress) { progress = false; if (act === 1 && CASE.actGate.every(id => pinned.has(id))) { act = 2; progress = true; } const ok = req => !req || req.every(r => pinned.has(r)); const grab = id => { if (!skipIds.has(id) && !pinned.has(id)) { pinned.add(id); return true; } return false; }; for (const l of CASE.locations) if ((l.act || 1) <= act) for (const sp of l.spots) if (sp.ev && grab(sp.ev)) progress = true; for (const s of CASE.systems) for (const q of s.queries) if (q.ev && (q.act || 1) <= act && ok(CASE.evidence[q.ev].requires) && grab(q.ev)) progress = true; for (const [pid, p] of Object.entries(CASE.people)) if (personAct[pid] <= act) for (const t of p.topics) if (t.gives && ok(t.requires) && grab(t.gives)) progress = true; } return { pinned, act }; } // повне проходження: все досяжно, акт 2 відкрився const full = simulate(); assert(full.act === 2, 'акт 2 не відкрився'); for (const id of Object.keys(CASE.evidence)) assert(full.pinned.has(id), `доказ "${id}" недосяжний`); // без доказів вбивства акт 2 не відкривається, і докази акту 2 недоступні const stuck = simulate(new Set(['decanter'])); assert(stuck.act === 1, 'акт 2 відкрився без доказу з actGate'); for (const [id, ev] of Object.entries(CASE.evidence)) if ((ev.act || 1) === 2) assert(!stuck.pinned.has(id), `доказ акту 2 "${id}" досяжний в акті 1`); // ---------- 4. Звинувачення ---------- assert(checkAccusation('stryzh', 'digoxin', ['blister', 'coatcheck', 'parking', 'dna_match']).ok); assert(checkAccusation('stryzh', 'digoxin', ['pharma_log', 'stryzh_lie', 'dna_match']).ok); assert(!checkAccusation('iryna', 'digoxin', ['blister', 'coatcheck', 'parking', 'dna_match']).ok); assert(!checkAccusation('stryzh', 'placebo', ['blister', 'coatcheck', 'parking', 'dna_match']).ok); assert(!checkAccusation('stryzh', 'digoxin', ['coatcheck', 'parking', 'dna_match']).ok); // без засобу assert(!checkAccusation('stryzh', 'digoxin', ['blister', 'parking', 'dna_match']).ok); // суперечність неповна assert(!checkAccusation('stryzh', 'digoxin', ['blister', 'coatcheck', 'parking']).ok); // без доступу for (const r of [checkAccusation('mazur', 'digoxin', []), checkAccusation('stryzh', 'natural', [])]) assert(!r.ok && r.hint.length > 10); // ключова суперечність вимагає обох ролей assert(CASE.evidence.coatcheck.role === 'field' && CASE.evidence.parking.role === 'analyst'); // перехід між актами вимагає обох ролей assert(new Set(CASE.actGate.map(id => CASE.evidence[id].role)).size === 2, 'actGate має вимагати доказів від обох ролей'); // ---------- 5. Міні-гра аналітика: код доступу ---------- assert.deepStrictEqual(seqGen(42, 5), seqGen(42, 5), 'seqGen недетермінований'); assert.notDeepStrictEqual(seqGen(42, 5), seqGen(43, 5), 'різні seed дають однакові коди'); for (const len of [4, 5, 6]) { const s = seqGen(123, len); assert(s.length === len); for (const v of s) assert(Number.isInteger(v) && v >= 0 && v < MG_SYMBOLS.length, 'символ поза межами'); } const seq = [0, 3, 1, 5]; assert(seqPrefixOk(seq, [])); assert(seqPrefixOk(seq, [0])); assert(seqPrefixOk(seq, [0, 3, 1])); assert(seqPrefixOk(seq, [0, 3, 1, 5])); assert(!seqPrefixOk(seq, [1])); assert(!seqPrefixOk(seq, [0, 3, 2])); // ---------- 6. Міні-гра польового: сканер ---------- const P = 1600; assert.strictEqual(triPos(0, P), 0); assert.strictEqual(triPos(P / 2, P), 100); assert.strictEqual(triPos(P, P), 0); assert.strictEqual(triPos(P / 4, P), 50); assert.strictEqual(triPos(3 * P / 4, P), 50); assert.strictEqual(triPos(P + P / 2, P), 100, 'періодичність зламана'); for (let t = -5000; t <= 5000; t += 37) { const p = triPos(t, P); assert(p >= 0 && p <= 100, `triPos(${t}) = ${p} поза [0,100]`); } assert(inZone(50, 40, 20)); assert(inZone(40, 40, 20) && inZone(60, 40, 20), 'межі зони мають зараховуватись'); assert(!inZone(39.99, 40, 20) && !inZone(60.01, 40, 20)); console.log('OK — кейс розв\'язний, акти працюють, міні-ігри перевірено.');