Pre-bugs fix

This commit is contained in:
UndecimoDia 2026-08-15 14:07:39 +03:00
parent 908252d5d9
commit 0af5aabb83
40 changed files with 257 additions and 120 deletions

View file

@ -1,105 +0,0 @@
# Розділ 3 — «Невпізнаний» (working title)
Co-op LAN detective, 2 players, Node + vanilla JS.
Every playthrough is a **new random case** from a seed-based generator. Inspired by Shadows of Doubt.
## Premise
A body with no ID is found in the district. You get only **starting facts** (randomized per case):
24 of e.g. a phone number on a scrap of paper, a footprint size, a fabric fiber, a tattoo,
a keyring, a partial fingerprint. From there:
- **Act I — Who died?** Identify the victim (DB queries, biometrics, missing persons, phone trace).
- **Act II — Who did it, how, and why?** The killer is NOT on any suspect gallery. You build a
profile from traces and cross-reference the citizen database until one person fits.
## Roles (asymmetric, both required)
### Польовий детектив — top-down 2D district
- Canvas top-down map of a **fixed hand-crafted compact district**: ~10 buildings
(apartment blocks, bar, diner, offices, shop, clinic, police HQ), walkable with WASD/arrows.
- Enters buildings, searches rooms via hotspots, collects **samples**: DNA swabs, lifted prints,
photos, documents, objects. Samples go to the shared board; analyst processes them.
- **Apartment access is legal-only**: public places open; private doors need a **key**
(found on the body, in a work safe, at a relative's — never just lying around) or a **warrant**
(requires pinned evidence pointing at that person/place).
- **Interrogation of any NPC** — but evidence-gated: topics unlock only when you hold the
relevant pinned evidence (ch2 `requires:` pattern, generated).
### Аналітик (поліцейський хакер) — the terminal
- **Citizen DB**: ~50 generated residents. Query by **typed name (keyboard — wrong spelling =
wrong/no dossier)** or filter by attributes (blood type, shoe size, employer, address, print type).
- **City map with addresses** — who lives/works where; mirrors the district and shows the
partner's live position.
- **Labs & systems** (each a hands-on minigame, not a button):
- **DNA lab**: gel electrophoresis — pipette sample into lanes, visually match band patterns
against DB candidates. Wrong match = confidently wrong lead.
- **Fingerprints**: overlay lifted print on candidate prints, rotate/align, mark ridge points.
DB narrows only by print *type*; the final match is your eyes.
- **CCTV archive**: scrub recorded footage of the crime night (timestamps, not a live clock),
spot a figure matching the profile (height, coat, gait).
- **Phone records + triangulation**: call logs for a number, triangulate a phone across
cell towers onto the city map.
- **Crypto/logic tasks** (analyst has a tech background — difficulty is allowed):
base64, Caesar/Vigenère and other classical ciphers, binary/hex conversion, **md5 password
cracking** for a suspect's email/laptop (weak password derivable from dossier facts: pet name +
birth year — detective work feeds the wordlist), and **one optional multi-stage Cicada3301-style
puzzle** guarding a bonus master-evidence cache. Puzzles are generated with the case.
## No clock
No time mechanic. Citizens have static current locations (at work or at home); all CCTV and
phone data is archival (the crime night). Actions cost nothing but thought.
## Case generator — standalone library
`chapter3/casegen/` — pure JS, no DOM/no server deps, reusable anywhere
(`generateCase(seed, opts)`):
- **Deterministic from seed**: same seed → same case. Needed for saves and both-players sync;
also lets you share a good case with friends.
- Generates: citizens (names, homes, jobs, relationships, biometrics, phones, current positions),
victim, killer, **motive** from template pool — jealousy/love triangle, debt/desperation,
blackmail turned fatal, silenced witness / mistaken identity, kidnapping-gone-wrong, and more
drawn from classic Holmes-style structures — method, and the full **evidence chain planted
backwards** from the solution to the starting facts.
- **Red herrings by design**: every case plants 12 plausible false leads (a citizen with motive
and no alibi, a second print at the scene). **Fair-play rule: every false lead is disprovable**
by reachable evidence — the game may mislead, never cheat.
- `opts` reserved for growth: `citizens`, `districts`, `difficulty` (only `seed` used at first).
- **Fairness validator in test.js**: for many random seeds, prove the chain from starting facts to
(victim identity, killer, motive, means) is reachable, and every red herring is refutable.
## Shared board & sync
- Server: Node `http` for static files + **`ws`** for real-time sync (state actions AND live
field-player position). Session stores the **seed** + full state, **persisted to a JSON file
per session code** — quit anytime, resume with the same code (the game is sized for 23 evenings).
- Evidence list and citizen/suspect list are both **interactive for both players**: tick,
cross out, highlight, hide, keyboard notes — all synced.
## Finale — one shot
Form with three pickers fed by the interactive lists: **ХТО** (from citizens),
**ЯК / ЧИМ** (method/weapon), **ЧОМУ** (motive), each link requiring attached evidence.
**Single accusation.** Submitting asks for explicit confirmation from *both* players.
Wrong → **you're fired**: the real case unfolds on screen, game over. Right → full recap, rank
based on evidence completeness and how few false leads you chased.
## Dependencies policy
Deps allowed, but only popular well-maintained packages, pinned versions, and only where stdlib
genuinely falls short. Current plan: **`ws` only.** Explicitly not used: game engines (canvas 2D
suffices), `express` (http.createServer suffices), `seedrandom` (mulberry32 is 5 lines),
md5 libs (`node:crypto`).
## Milestones
1. `casegen` lib + fairness tests (the heart — everything else renders its output)
2. Server: `ws` sync, sessions + seed + disk saves
3. Analyst terminal: DB, typed queries, city map
4. Field: top-down 2D district, movement, search, samples, keys/warrants
5. Minigames: DNA gel, fingerprints, CCTV, triangulation
6. Crypto tasks + Cicada puzzle
7. Interrogations, finale form, firing ending, polish, README

147
detective-game/.gitignore vendored Normal file
View file

@ -0,0 +1,147 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
.output
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp directory
.temp
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# pnpm
.pnpm-store
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/
saves/
bugs.md
DESIGN.md

View file

@ -3,6 +3,7 @@
// граф доказів (спланований від розв'язку назад), хибні сліди (кожен — спростовний),
// шифро-завдання. solve() — симулятор досяжності для fair-play тестів.
const crypto = require('crypto');
const { makeDistrict, unitBuilding } = require('./district');
const N = require('./names');
const { hashSeed, mulberry32, ri, estRange, pick, chance, shuffle, UKR, caesar, b64, md5, translit, COMMON_PASSWORDS, genPassword } = require('./rng');
@ -32,6 +33,10 @@ const { applyDoppelganger } = require('./mutators');
const { generateAnonymousLetter } = require('./anonymous_letter');
// ---------- Генерація справи ----------
// Код кімнати (сесія) → сід справи. Хешуємо, щоб близькі коди («ABC», «ABC1»)
// давали геть різні справи. Єдине джерело правди: і сервер, і тести сіють цим.
function caseSeed(code) { return crypto.createHash('sha512').update(String(code)).digest('hex'); }
function generateCase(seed) {
const r = mulberry32(hashSeed(seed));
const district = makeDistrict(r);
@ -625,7 +630,7 @@ function generateCase(seed) {
id: 'lab_print_match', side: 'analyst', at: { type: 'lab' }, requires: ['ev_weapon'],
title: 'Повний відбиток зі знаряддя', tag: 'вбивця',
detail: `Відбиток зі знаряддя порівняно з базою (звузьте кандидатів і зіставте вручну). Збіг за ${ri(r, 12, 16)} точками: ${killer.name}.`,
gives: { confirms: true }
gives: { confirms: true, pointsTo: killer.id } // чіткий доказ: годиться і в звинувачення, і на ордер
});
if (!(caseType === 'murder' && mutator === 'gloves')) add({
id: 'lab_dna_match', side: 'analyst', at: { type: 'lab' }, requires: ['sf_dna'], special: 'dna',
@ -634,7 +639,9 @@ function generateCase(seed) {
? `Електрофорез (${dnaDesc}): повний збіг доріжок — ${accomplice.name}. Дивно: на кого вказують взуття і тканина — це ІНША людина.`
: `Електрофорез (${dnaDesc}): повний збіг доріжок — ${killer.name}.`) +
' Коли і чому ДНК потрапила на місце — аналіз не скаже.',
gives: mutator === 'accomplice' ? { pointsTo: accomplice.id } : { confirms: true }
// accomplice: ДНК належить спільнику → вказує на нього (твіст лишаємо).
// інакше збіг прямо називає вбивцю → чіткий доказ: і в звинувачення, і на ордер.
gives: mutator === 'accomplice' ? { pointsTo: accomplice.id } : { confirms: true, pointsTo: killer.id }
});
if (dna2) add({
id: 'lab_dna_match2', side: 'analyst', at: { type: 'lab' }, requires: ['sf_dna2'], special: 'dna2',
@ -692,6 +699,12 @@ function generateCase(seed) {
`${mutator === 'gloves' ? 'повний відбиток зі знаряддя' : 'ДНК з місця'} і камера, що впіймала зріст. База мешканців зробила решту. Справу закрито.`
};
// Мотив у прямій мові + монетка 50/50: чи проговорить вбивця мотив на зізнанні
// (для реалізму). Draw ОСТАННІЙ у генерації — після нього r не чіпаємо, тож
// наявні сейви лишаються тією ж справою, лише з новими полями фіналу.
finale.recapWhy = recapWhy;
finale.revealsMotive = chance(r, 0.5);
// admit — пряма мова хибного підозрюваного (див. FM_KINDS), а не третьоособовий
// зворот: цитуємо, а не вклеюємо в речення.
finale.recap += ` ${herring.first} визна${g(herring, 'в', 'ла')}: «${fm.admit}» — та до злочину стосунку не ${g(herring, 'мав', 'мала')}.`;
@ -712,4 +725,4 @@ function generateCase(seed) {
};
}
module.exports = { generateCase, solve, checkAccusation, caesar, md5, translit, COMMON_PASSWORDS };
module.exports = { generateCase, caseSeed, solve, checkAccusation, caesar, md5, translit, COMMON_PASSWORDS };

View file

@ -267,14 +267,24 @@
if (!claims.length) { box.innerHTML = 'Ще нікого не питали, де він був того вечора.'; box.className = 'small dim'; return; }
box.className = '';
const bn = id => { const b = App.init.district.buildings.find(x => x.id === id); return b ? b.name.split(',')[0] : id; };
const t = el('table', '', '<tr><th>Хто</th><th>Каже, що був</th><th>Камери над тим місцем</th></tr>');
const t = el('table', '', '<tr><th>Хто</th><th>Каже, що був</th><th>Камери над тим місцем</th><th></th></tr>');
for (const [cid, claim] of claims) {
const c = App.byId(cid);
const cams = App.init.district.cameras.filter(cam => cam.covers.includes(claim));
// Той самий ключ, що й у базі мешканців: галочка/викреслення на особі — спільні.
const key = 'c:' + cid;
if (App.state.board.marks[key] === 'hide') continue; // сховане в базі — сховане й тут
// Зізнався, що збрехав → показуємо перехід «стара заява → реальне місце»,
// і камери підтягуємо вже над реальним місцем (обидві колонки синхронно).
const real = (App.state.alibiReal || {})[cid];
const place = real || claim;
const said = real ? `${esc(bn(claim))}${esc(bn(real))}` : esc(bn(claim));
const cams = App.init.district.cameras.filter(cam => cam.covers.includes(place));
const cell = cams.length
? cams.map(cam => `${esc(cam.name)}${(App.state.cctvPulled || []).includes(cam.id) ? ' — архів на дошці' : ' — архів не тягнули'}`).join('<br>')
: 'жодна камера туди не дивиться';
t.append(el('tr', '', `<td>${esc(c.name)}</td><td>${esc(bn(claim))}</td><td class="small">${cell}</td>`));
const tr = el('tr', cardClasses(key), `<td>${esc(c.name)}</td><td>${said}</td><td class="small">${cell}</td>`);
const mk = el('td'); mk.append(markButtons(key)); tr.append(mk);
t.append(tr);
}
box.innerHTML = ''; box.append(t);
}

View file

@ -178,6 +178,23 @@ function cardClasses(key) {
return m && m !== "hide" ? " " + m : "";
}
// Прокрутити дошку до доказу за id і коротко підсвітити. Якщо він у згорнутій
// секції «Приховані» — спершу розгортаємо її й перемальовуємо.
function jumpToEvidence(id) {
const find = () =>
document.querySelector(`#sidebody [data-ev="${CSS.escape(id)}"]`);
let card = find();
if (!card && !showHidden) {
showHidden = true;
renderSide();
card = find();
}
if (!card) return;
card.scrollIntoView({ behavior: "smooth", block: "center" });
card.classList.add("flash");
setTimeout(() => card.classList.remove("flash"), 1300);
}
function renderSide() {
if (!App.state) return;
const body = $("#sidebody");
@ -211,6 +228,7 @@ function renderSide() {
const buildCard = (e) => {
const key = "e:" + e.id;
const c = el("div", "card" + cardClasses(key));
c.dataset.ev = e.id; // якір для прокрутки-до-доказу
const t = el("div", "t");
t.append(el("b", "", esc(e.title)));
const pin = el(
@ -220,7 +238,17 @@ function renderSide() {
);
pin.onclick = () => App.send({ t: "pin", evId: e.id });
t.append(pin);
c.append(t, el("div", "small", esc(e.detail)), markButtons(key));
c.append(t, el("div", "small", esc(e.detail)));
// Експертиза називає доказ, який розбирала, і веде до нього (прокрутка + підсвітка).
if (e.ref) {
const src = found.find((x) => x.id === e.ref);
if (src) {
const link = el("button", "evref small", `↳ аналіз доказу: «${esc(src.title)}»`);
link.onclick = () => jumpToEvidence(e.ref);
c.append(link);
}
}
c.append(markButtons(key));
const notes = (App.state.board.enotes || {})[e.id] || [];
for (const n of notes) {
const row = el(

View file

@ -2,6 +2,7 @@
"use strict";
(() => {
let cv, ctx, hint;
let started = false; // слухачі/цикл/розсилку ставимо раз, навіть якщо init прийде знову
let player = { x: 85, y: 290 };
const keys = {};
let solids = [],
@ -168,11 +169,16 @@
stage.append(cv, hint);
ctx = cv.getContext("2d");
fitCanvas(cv);
addEventListener("resize", () => fitCanvas(cv));
solids = m.district.buildings
.filter((b) => !OPEN_ZONES.includes(b.id))
.map((b) => b.pos);
walkIn = m.district.buildings.filter((b) => OPEN_ZONES.includes(b.id));
// init прилітає й на реконекті: канвас/стан перебудували вище, а глобальні
// слухачі, цикл рендеру й розсилку позицій ставимо ЛИШЕ раз — інакше після
// кожного реконекту крутився б зайвий loop і дублювались клавіші.
if (started) return;
started = true;
addEventListener("resize", () => fitCanvas(cv));
// e.code — фізична клавіша, працює на будь-якій розкладці (укр/лат)
addEventListener("keydown", (e) => {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")

View file

@ -89,6 +89,9 @@
#side .tag { font-size:10.5px; background:transparent; border:1px solid #b3a077; color:var(--inkdim); border-radius:2px; padding:0 5px; }
.card .t { display:flex; gap:6px; align-items:baseline; }
.card .t b { flex:1; }
.evref { display:block; width:100%; text-align:left; margin-top:4px; font-style:italic; }
@keyframes evflash { 0%,100% { box-shadow:0 4px 10px rgba(0,0,0,.55); } 25%,60% { box-shadow:0 0 0 3px var(--acc), 0 4px 14px rgba(0,0,0,.6); } }
#side .card.flash { animation:evflash 1.3s ease; }
.mk { display:flex; gap:4px; margin-top:6px; }
.mk button { padding:1px 8px; font-size:12px; }
.mk button.on { border-color:#7a1f1f; color:#7a1f1f; font-weight:bold; }

View file

@ -4,9 +4,8 @@ const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');
const crypto = require('crypto');
const { WebSocketServer } = require('ws');
const { generateCase, checkAccusation, md5 } = require('./casegen');
const { generateCase, caseSeed, checkAccusation, md5 } = require('./casegen');
const { unitBuilding } = require('./casegen/district');
const RT = require('./casegen/runtime');
@ -40,7 +39,7 @@ function seedRand(seed) {
function getSession(code) {
if (sessions[code]) return sessions[code];
const hashedCode = crypto.createHash('sha512').update(code).digest('hex');
const hashedCode = caseSeed(code);
const cs = generateCase(hashedCode);
const rng = seedRand([...hashedCode].reduce((a, c) => a * 31 + c.charCodeAt(0), 0));
const stn = cs.district.buildings.find(b => b.id === 'station');
@ -131,7 +130,11 @@ function view(s) {
const { cs, st } = s;
const found = st.found.map(id => {
const ev = cs.evidence[id];
return { id, title: ev.title, detail: ev.detail, side: ev.side };
const card = { id, title: ev.title, detail: ev.detail, side: ev.side };
// Експертиза (crim/expert) розбирає конкретний доказ — його id у requires[0].
// Віддаємо як ref, щоб на дошці було видно, ЧИЙ це аналіз, і клік вів до нього.
if (ev.q && ev.q.kind === 'expert' && ev.requires[0]) card.ref = ev.requires[0];
return card;
}).concat(s.extra);
const killerC = cs.citizens.find(c => c.id === cs.finale.killer);
const dnaOwner = cs.citizens.find(c => c.id === (cs.accomplice || cs.finale.killer));
@ -143,6 +146,12 @@ function view(s) {
puzzles: visiblePuzzles(s),
locs: s.locs, markers: s.markers, log: s.log,
alibiClaims: s.alibiClaims,
// Хто вже зізнався, що збрехав про вечір → його СПРАВЖНЄ місце (c.evening).
// Клієнт правди не має (evening вирізано в pubCitizens), тож віддаємо тут —
// тільки для розколотих, тож нічого понад уже сказане в зізнанні не тече.
alibiReal: Object.fromEntries(Object.keys(s.alibiClaims)
.filter(cid => s.cracked[cid])
.map(cid => [cid, byIdC(cs, cid).evening])),
cctvPulled: s.extra.filter(x => x.key && x.key.startsWith('cctv|')).map(x => x.key.slice(5)),
dnaSample: st.found.includes('sf_dna') ? dnaOwner.dna : null,
dnaLabel: st.found.includes('sf_dna') ? cs.dnaDesc : null,
@ -565,8 +574,19 @@ function handle(s, ws, m) {
`${c.name} впізнає слід біля місця злочину: ${g(c, 'проходив', 'проходила')} там того вечора. До справи відношення не має.`);
} else if (K && gives.confirms) {
a = `Довга пауза. «...Гаразд. Ваша взяла.» — і ${c.name} починає говорити.`;
// Зізнання підтверджує ХТО/ЯК, але мотив цим не доводиться: прокурор вимагає
// ЗАКРІПЛЕНИЙ доказ мотиву (accusation.js). Тож картка чесно каже, що ЧОМУ —
// ще відкрите, інакше гравець думає, що вже все, і зависає на звинуваченні.
const motiveBacked = st.found.some(id => cs.evidence[id] && cs.evidence[id].gives && cs.evidence[id].gives.motive);
const whyLine = motiveBacked
? `Мотив уже підкріплено доказом. Лишилося оформити справу: ХТО / ЯК / ЧОМУ і закріпити докази для прокурора.`
// 50/50 (вирішено при генерації справи): інколи вбивця сам проговорює мотив.
// Але слова — ще не доказ: прокурор усе одно вимагає ЗАКРІПЛЕНИЙ доказ мотиву.
: cs.finale.revealsMotive && cs.finale.recapWhy
? `Цього разу ${g(c, 'він заговорив', 'вона заговорила')} і про мотив: ${cs.finale.recapWhy} Та зізнання на словах — ще не доказ: щоб оформити ЧОМУ, знайдіть і закріпіть доказ мотиву.`
: `Але про мотив мовчить: ЧОМУ ще не доведено. Прокурор не прийме справу без ЗАКРІПЛЕНОГО доказу мотиву — знайдіть і закріпіть його, тоді оформлюйте ХТО / ЯК / ЧОМУ.`;
extraCard(s, 'confess', `Зізнання: ${c.name}`,
`Після пред'явлення (${ev.title.toLowerCase()}) ${c.name} ${g(c, 'зізнався', 'зізналася')}. Протокол підписано. Лишилося оформити справу: ХТО / ЯК / ЧОМУ — і закріпити докази для прокурора.`);
`Після пред'явлення (${ev.title.toLowerCase()}) ${c.name} ${g(c, 'зізнався', 'зізналася')} у скоєному. Протокол підписано. ${whyLine}`);
} else if (cs.dna2 && c.id === cs.dna2.owner && ['sf_dna2', 'lab_dna_match2'].includes(m.evId)) {
// невинна ДНК: людина пояснює, звідки вона там, — вірити чи ні, вирішують гравці
const vic = byIdC(cs, cs.victim);
@ -751,8 +771,23 @@ function pubCitizens(cs) {
}
const wss = new WebSocketServer({ server });
// Heartbeat: без нього обірване з'єднання (сон ноута, зміна Wi-Fi, kill) лишає
// мертвий сокет у s.clients назавжди — застигла точка на мапі, «Польовий2» за
// одного гравця, роздутий лічильник. Пінгуємо всіх; хто не відповів між тіками —
// terminate, і штатний 'close' прибирає його з сесії.
const HEARTBEAT_MS = +process.env.HEARTBEAT_MS || 30000;
const heartbeat = setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) { ws.terminate(); continue; }
ws.isAlive = false;
try { ws.ping(); } catch { }
}
}, HEARTBEAT_MS);
wss.on('close', () => clearInterval(heartbeat));
wss.on('connection', ws => {
ws.id = ++clientSeq;
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('message', raw => {
let m; try { m = JSON.parse(raw); } catch { return; }
if (m.t === 'join') {

View file

@ -5,7 +5,7 @@ const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const WebSocket = require('ws');
const { generateCase } = require('./casegen');
const { generateCase, caseSeed } = require('./casegen');
const PORT = +process.env.PORT || 3102;
const NO_SPAWN = !!process.env.NO_SPAWN; // підключитись до вже запущеного сервера (для дебагу з логами)
@ -43,7 +43,7 @@ function solveTriangulate(cs, towers) {
}
async function playToTheEnd(session, accusation) {
const cs = generateCase(session);
const cs = generateCase(caseSeed(session)); // сервер сіє хешованим кодом — тест мусить так само
const killer = cs.citizens.find(c => c.id === cs.finale.killer);
const field = client('field'); field.session = session;
const analyst = client('analyst'); analyst.session = session;