2PlayerGame/chapter2/test.js

108 lines
6.8 KiB
JavaScript
Raw Normal View History

2026-07-23 17:03:26 +03:00
// Перевірки розділу 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 — кейс розв\'язний, акти працюють, міні-ігри перевірено.');