107 lines
6.8 KiB
JavaScript
107 lines
6.8 KiB
JavaScript
// Перевірки розділу 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 — кейс розв\'язний, акти працюють, міні-ігри перевірено.');
|