M0: план разработки, техническая структура, каркас проекта

- docs/PLAN.md — план по этапам, обновляется по мере работы
- docs/TECHNICAL.md — слои, карта папок, контракты, рабочие процессы
- docs/DECISIONS.md — журнал решений, принятых самостоятельно
- разрешение 1080x1920, fit-width (портрет, 4:3..21:9)
- каркас assets/scripts (core / modules / app / ui)
- headless-тесты на vitest + strict typecheck + защита чистоты core
- Rng: детерминированный генератор и перемешивание

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ftc
2026-09-13 10:28:49 +03:00
co-authored by Claude Opus 5
parent 47e59eab38
commit c980440856
31 changed files with 2333 additions and 1 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "c56646a3-5861-441d-b174-0b9712e86765",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "b130857b-d0fd-47d0-b6a7-72a6d2faafd6",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "f005faf0-2224-4396-89d7-763a71c16ab9",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "47d05b64-ed47-445a-9fce-33a0253484c1",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "008f0d07-3e06-4f0f-b557-a30fa1447a2a",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "998dfbeb-890a-4444-b01a-b8f5c92223ef",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "0c2ba096-89cd-4e81-bb73-8a02831db975",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "84ea36f8-e700-4315-8516-373d82c81cd6",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "d6eb348d-52cc-40e0-9de8-e3018ec92af2",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "2690e23c-9deb-4acd-818d-bec476786aad",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "649ca5c0-9d88-48ee-8cb4-269768037b5d",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3bc478de-2530-4d7a-ac17-861f3347644e",
"files": [],
"subMetas": {},
"userData": {}
}
+65
View File
@@ -0,0 +1,65 @@
/**
* Deterministic pseudo-random generator.
*
* The whole rules core is reproducible from a seed: given the same seed and the
* same sequence of actions, a match replays identically. That is what lets a bug
* report be a single integer, and what keeps the soak tests meaningful.
*
* Algorithm is mulberry32 — small, fast, and good enough for shuffling cards.
* Its entire state is one 32-bit integer, so it serialises into the save/state
* without ceremony.
*/
export class Rng {
private _state: number;
constructor(seed: number) {
// Force to uint32 so a negative or fractional seed still behaves.
this._state = seed >>> 0;
}
/** Current internal state, for serialising a game in progress. */
get state(): number {
return this._state;
}
/** Restores a generator to a previously captured state. */
static fromState(state: number): Rng {
return new Rng(state);
}
/** Uniform float in [0, 1). */
next(): number {
this._state = (this._state + 0x6d2b79f5) >>> 0;
let t = this._state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
/** Uniform integer in [0, bound). `bound` must be positive. */
nextInt(bound: number): number {
if (bound <= 0) {
throw new RangeError(`Rng.nextInt bound must be positive, got ${bound}`);
}
return Math.floor(this.next() * bound);
}
/** Uniformly picks one element. Throws on an empty array. */
pick<T>(items: readonly T[]): T {
if (items.length === 0) {
throw new RangeError('Rng.pick called on an empty array');
}
return items[this.nextInt(items.length)];
}
/** Fisher-Yates, in place. Returns the same array for convenience. */
shuffle<T>(items: T[]): T[] {
for (let i = items.length - 1; i > 0; i--) {
const j = this.nextInt(i + 1);
const tmp = items[i];
items[i] = items[j];
items[j] = tmp;
}
return items;
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e9145215-b0c0-46a4-83b9-caf393a20cc1",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "ce55fda9-c26d-4c2b-9baa-d7e8e5e1f49b",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "b0733617-74e3-450d-9dfd-97cf9ef75e93",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "d6076a47-71fc-41a3-82a7-a5a59d79a52e",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6c370e15-1daf-4bea-99fa-29acbaa3e03c",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "bc778720-1b57-4cc0-b7d1-3c4f8dcdc51d",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "f2b25917-c1a9-486b-b671-760782f37bcd",
"files": [],
"subMetas": {},
"userData": {}
}