diff --git a/chapter3/DESIGN.md b/chapter3/DESIGN.md deleted file mode 100644 index b351cc8..0000000 --- a/chapter3/DESIGN.md +++ /dev/null @@ -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): -2–4 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 1–2 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 2–3 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 diff --git a/detective-game/.gitignore b/detective-game/.gitignore new file mode 100644 index 0000000..19ed07e --- /dev/null +++ b/detective-game/.gitignore @@ -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 diff --git a/chapter3/README.md b/detective-game/README.md similarity index 100% rename from chapter3/README.md rename to detective-game/README.md diff --git a/chapter3/casegen/accusation.js b/detective-game/casegen/accusation.js similarity index 100% rename from chapter3/casegen/accusation.js rename to detective-game/casegen/accusation.js diff --git a/chapter3/casegen/anonymous_letter.js b/detective-game/casegen/anonymous_letter.js similarity index 100% rename from chapter3/casegen/anonymous_letter.js rename to detective-game/casegen/anonymous_letter.js diff --git a/chapter3/casegen/citizens.js b/detective-game/casegen/citizens.js similarity index 100% rename from chapter3/casegen/citizens.js rename to detective-game/casegen/citizens.js diff --git a/chapter3/casegen/constants.js b/detective-game/casegen/constants.js similarity index 100% rename from chapter3/casegen/constants.js rename to detective-game/casegen/constants.js diff --git a/chapter3/casegen/devices.js b/detective-game/casegen/devices.js similarity index 100% rename from chapter3/casegen/devices.js rename to detective-game/casegen/devices.js diff --git a/chapter3/casegen/district.js b/detective-game/casegen/district.js similarity index 100% rename from chapter3/casegen/district.js rename to detective-game/casegen/district.js diff --git a/chapter3/casegen/evening.js b/detective-game/casegen/evening.js similarity index 100% rename from chapter3/casegen/evening.js rename to detective-game/casegen/evening.js diff --git a/chapter3/casegen/index.js b/detective-game/casegen/index.js similarity index 97% rename from chapter3/casegen/index.js rename to detective-game/casegen/index.js index 55c536e..dc603ee 100644 --- a/chapter3/casegen/index.js +++ b/detective-game/casegen/index.js @@ -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 }; diff --git a/chapter3/casegen/junk.js b/detective-game/casegen/junk.js similarity index 100% rename from chapter3/casegen/junk.js rename to detective-game/casegen/junk.js diff --git a/chapter3/casegen/motives/blackmail.js b/detective-game/casegen/motives/blackmail.js similarity index 100% rename from chapter3/casegen/motives/blackmail.js rename to detective-game/casegen/motives/blackmail.js diff --git a/chapter3/casegen/motives/business.js b/detective-game/casegen/motives/business.js similarity index 100% rename from chapter3/casegen/motives/business.js rename to detective-game/casegen/motives/business.js diff --git a/chapter3/casegen/motives/debt.js b/detective-game/casegen/motives/debt.js similarity index 100% rename from chapter3/casegen/motives/debt.js rename to detective-game/casegen/motives/debt.js diff --git a/chapter3/casegen/motives/inherit.js b/detective-game/casegen/motives/inherit.js similarity index 100% rename from chapter3/casegen/motives/inherit.js rename to detective-game/casegen/motives/inherit.js diff --git a/chapter3/casegen/motives/jealousy.js b/detective-game/casegen/motives/jealousy.js similarity index 100% rename from chapter3/casegen/motives/jealousy.js rename to detective-game/casegen/motives/jealousy.js diff --git a/chapter3/casegen/motives/kidnap_debt.js b/detective-game/casegen/motives/kidnap_debt.js similarity index 100% rename from chapter3/casegen/motives/kidnap_debt.js rename to detective-game/casegen/motives/kidnap_debt.js diff --git a/chapter3/casegen/motives/kidnap_obsession.js b/detective-game/casegen/motives/kidnap_obsession.js similarity index 100% rename from chapter3/casegen/motives/kidnap_obsession.js rename to detective-game/casegen/motives/kidnap_obsession.js diff --git a/chapter3/casegen/motives/kidnap_ransom.js b/detective-game/casegen/motives/kidnap_ransom.js similarity index 100% rename from chapter3/casegen/motives/kidnap_ransom.js rename to detective-game/casegen/motives/kidnap_ransom.js diff --git a/chapter3/casegen/motives/mistaken.js b/detective-game/casegen/motives/mistaken.js similarity index 100% rename from chapter3/casegen/motives/mistaken.js rename to detective-game/casegen/motives/mistaken.js diff --git a/chapter3/casegen/motives/oldcrime.js b/detective-game/casegen/motives/oldcrime.js similarity index 100% rename from chapter3/casegen/motives/oldcrime.js rename to detective-game/casegen/motives/oldcrime.js diff --git a/chapter3/casegen/motives/ransom.js b/detective-game/casegen/motives/ransom.js similarity index 100% rename from chapter3/casegen/motives/ransom.js rename to detective-game/casegen/motives/ransom.js diff --git a/chapter3/casegen/motives/serial.js b/detective-game/casegen/motives/serial.js similarity index 100% rename from chapter3/casegen/motives/serial.js rename to detective-game/casegen/motives/serial.js diff --git a/chapter3/casegen/motives/stalker.js b/detective-game/casegen/motives/stalker.js similarity index 100% rename from chapter3/casegen/motives/stalker.js rename to detective-game/casegen/motives/stalker.js diff --git a/chapter3/casegen/motives/witness.js b/detective-game/casegen/motives/witness.js similarity index 100% rename from chapter3/casegen/motives/witness.js rename to detective-game/casegen/motives/witness.js diff --git a/chapter3/casegen/mutators.js b/detective-game/casegen/mutators.js similarity index 100% rename from chapter3/casegen/mutators.js rename to detective-game/casegen/mutators.js diff --git a/chapter3/casegen/names.js b/detective-game/casegen/names.js similarity index 100% rename from chapter3/casegen/names.js rename to detective-game/casegen/names.js diff --git a/chapter3/casegen/rng.js b/detective-game/casegen/rng.js similarity index 100% rename from chapter3/casegen/rng.js rename to detective-game/casegen/rng.js diff --git a/chapter3/casegen/runtime.js b/detective-game/casegen/runtime.js similarity index 100% rename from chapter3/casegen/runtime.js rename to detective-game/casegen/runtime.js diff --git a/chapter3/casegen/test.js b/detective-game/casegen/test.js similarity index 100% rename from chapter3/casegen/test.js rename to detective-game/casegen/test.js diff --git a/chapter3/package-lock.json b/detective-game/package-lock.json similarity index 100% rename from chapter3/package-lock.json rename to detective-game/package-lock.json diff --git a/chapter3/package.json b/detective-game/package.json similarity index 100% rename from chapter3/package.json rename to detective-game/package.json diff --git a/chapter3/public/analyst.js b/detective-game/public/analyst.js similarity index 96% rename from chapter3/public/analyst.js rename to detective-game/public/analyst.js index a69f7bf..427cf66 100644 --- a/chapter3/public/analyst.js +++ b/detective-game/public/analyst.js @@ -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', '', '