@its-not-rocket-science/ananke 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,19 @@ Versioning follows [Semantic Versioning](https://semver.org/).
10
10
 
11
11
  ---
12
12
 
13
+ ## [0.1.2] — 2026-03-19
14
+
15
+ ### Added
16
+
17
+ - `createWorld(seed, entities)` — Tier-1 convenience factory; builds a `WorldState` from
18
+ `EntitySpec[]` (archetype, weapon, armour string IDs) without manual entity construction
19
+ - `loadScenario(json)` / `validateScenario(json)` — JSON-driven world creation for
20
+ non-TypeScript consumers (Godot GDScript, Unity C#, scenario files)
21
+ - `ARCHETYPE_MAP` — `ReadonlyMap` of all 21 built-in archetypes (7 base + 14 species)
22
+ - `ITEM_MAP` — `ReadonlyMap` of all historical and starter weapons/armour
23
+
24
+ ---
25
+
13
26
  ## [0.1.1] — 2026-03-19
14
27
 
15
28
  ### Documentation
@@ -40,3 +40,5 @@ export * from "./sim/impairment.js";
40
40
  export * from "./sim/indexing.js";
41
41
  export * from "./sim/tuning.js";
42
42
  export * from "./sim/testing.js";
43
+ export * from "./world-factory.js";
44
+ export * from "./scenario.js";
package/dist/src/index.js CHANGED
@@ -52,3 +52,5 @@ export * from "./sim/impairment.js"; // low-level impairment accumulators
52
52
  export * from "./sim/indexing.js"; // SpatialIndex internals
53
53
  export * from "./sim/tuning.js"; // kernel tuning constants (may be adjusted)
54
54
  export * from "./sim/testing.js"; // mkHumanoidEntity() and other test helpers
55
+ export * from "./world-factory.js"; // createWorld(), EntitySpec, ARCHETYPE_MAP, ITEM_MAP
56
+ export * from "./scenario.js"; // loadScenario(), validateScenario(), AnankeScenario
@@ -0,0 +1,37 @@
1
+ /**
2
+ * CE-3: JSON scenario loader.
3
+ *
4
+ * Provides typed AnankeScenario interface, structural validation, and
5
+ * a loadScenario() function that converts validated JSON into a WorldState.
6
+ */
7
+ import type { WorldState } from "./sim/world.js";
8
+ export interface AnankeScenarioEntity {
9
+ id: number;
10
+ teamId: number;
11
+ archetype: string;
12
+ weapon: string;
13
+ armour?: string;
14
+ x_m?: number;
15
+ y_m?: number;
16
+ }
17
+ export interface AnankeScenario {
18
+ $schema?: string;
19
+ id: string;
20
+ seed: number;
21
+ maxTicks: number;
22
+ tractionCoeff?: number;
23
+ entities: AnankeScenarioEntity[];
24
+ }
25
+ /**
26
+ * Validate structural correctness of a JSON scenario object.
27
+ * Returns an array of error strings — empty array means valid.
28
+ * Does NOT perform simulation-level lookups (e.g. archetype/weapon existence).
29
+ */
30
+ export declare function validateScenario(json: unknown): string[];
31
+ /**
32
+ * Parse and load a scenario from JSON, returning a WorldState ready for stepWorld().
33
+ *
34
+ * Calls validateScenario first — throws an Error with all validation messages if invalid.
35
+ * Maps AnankeScenarioEntity.id as the entity seed.
36
+ */
37
+ export declare function loadScenario(json: unknown): WorldState;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * CE-3: JSON scenario loader.
3
+ *
4
+ * Provides typed AnankeScenario interface, structural validation, and
5
+ * a loadScenario() function that converts validated JSON into a WorldState.
6
+ */
7
+ import { createWorld } from "./world-factory.js";
8
+ // ── Validation ────────────────────────────────────────────────────────────────
9
+ /**
10
+ * Validate structural correctness of a JSON scenario object.
11
+ * Returns an array of error strings — empty array means valid.
12
+ * Does NOT perform simulation-level lookups (e.g. archetype/weapon existence).
13
+ */
14
+ export function validateScenario(json) {
15
+ const errors = [];
16
+ // Must be a plain object
17
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
18
+ errors.push("scenario must be a plain object");
19
+ return errors; // can't continue checking fields
20
+ }
21
+ const obj = json;
22
+ // id — non-empty string
23
+ if (typeof obj["id"] !== "string" || obj["id"].length === 0) {
24
+ errors.push("scenario.id must be a non-empty string");
25
+ }
26
+ // seed — positive integer
27
+ if (typeof obj["seed"] !== "number" ||
28
+ !Number.isInteger(obj["seed"]) ||
29
+ obj["seed"] <= 0) {
30
+ errors.push("scenario.seed must be a positive integer");
31
+ }
32
+ // maxTicks — positive integer
33
+ if (typeof obj["maxTicks"] !== "number" ||
34
+ !Number.isInteger(obj["maxTicks"]) ||
35
+ obj["maxTicks"] <= 0) {
36
+ errors.push("scenario.maxTicks must be a positive integer");
37
+ }
38
+ // entities — non-empty array
39
+ if (!Array.isArray(obj["entities"])) {
40
+ errors.push("scenario.entities must be an array");
41
+ return errors; // can't check entity elements
42
+ }
43
+ const rawEntities = obj["entities"];
44
+ if (rawEntities.length === 0) {
45
+ errors.push("scenario.entities must not be empty");
46
+ return errors;
47
+ }
48
+ // Validate each entity element
49
+ const seenIds = new Set();
50
+ for (let i = 0; i < rawEntities.length; i++) {
51
+ const ent = rawEntities[i];
52
+ if (ent === null || typeof ent !== "object" || Array.isArray(ent)) {
53
+ errors.push(`scenario.entities[${i}] must be a plain object`);
54
+ continue;
55
+ }
56
+ const e = ent;
57
+ if (typeof e["id"] !== "number") {
58
+ errors.push(`scenario.entities[${i}].id must be a number`);
59
+ }
60
+ else {
61
+ const eid = e["id"];
62
+ if (seenIds.has(eid)) {
63
+ errors.push(`scenario.entities[${i}].id ${eid} is a duplicate`);
64
+ }
65
+ seenIds.add(eid);
66
+ }
67
+ if (typeof e["teamId"] !== "number") {
68
+ errors.push(`scenario.entities[${i}].teamId must be a number`);
69
+ }
70
+ if (typeof e["archetype"] !== "string") {
71
+ errors.push(`scenario.entities[${i}].archetype must be a string`);
72
+ }
73
+ if (typeof e["weapon"] !== "string") {
74
+ errors.push(`scenario.entities[${i}].weapon must be a string`);
75
+ }
76
+ }
77
+ return errors;
78
+ }
79
+ // ── Loader ────────────────────────────────────────────────────────────────────
80
+ /**
81
+ * Parse and load a scenario from JSON, returning a WorldState ready for stepWorld().
82
+ *
83
+ * Calls validateScenario first — throws an Error with all validation messages if invalid.
84
+ * Maps AnankeScenarioEntity.id as the entity seed.
85
+ */
86
+ export function loadScenario(json) {
87
+ const errors = validateScenario(json);
88
+ if (errors.length > 0) {
89
+ throw new Error(`loadScenario: invalid scenario:\n ${errors.join("\n ")}`);
90
+ }
91
+ const scenario = json;
92
+ const specs = scenario.entities.map(ent => {
93
+ const spec = {
94
+ id: ent.id,
95
+ teamId: ent.teamId,
96
+ seed: ent.id, // use entity id as deterministic seed
97
+ archetype: ent.archetype,
98
+ weaponId: ent.weapon,
99
+ };
100
+ if (ent.armour !== undefined)
101
+ spec.armourId = ent.armour;
102
+ if (ent.x_m !== undefined)
103
+ spec.x_m = ent.x_m;
104
+ if (ent.y_m !== undefined)
105
+ spec.y_m = ent.y_m;
106
+ return spec;
107
+ });
108
+ return createWorld(scenario.seed, specs);
109
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CE-2: createWorld convenience factory.
3
+ *
4
+ * Builds a deterministic WorldState from a simple declarative entity spec.
5
+ * No Math.random() — all randomness flows through generateIndividual(spec.seed, archetype).
6
+ * All position coordinates are fixed-point (SCALE.m multiplier + Math.round).
7
+ */
8
+ import type { Archetype } from "./archetypes.js";
9
+ import type { Item } from "./equipment.js";
10
+ import type { WorldState } from "./sim/world.js";
11
+ /** Map of string keys to Archetype objects for use with createWorld(). */
12
+ export declare const ARCHETYPE_MAP: ReadonlyMap<string, Archetype>;
13
+ /** Map of item id → Item for weapons and armour usable with createWorld(). */
14
+ export declare const ITEM_MAP: ReadonlyMap<string, Item>;
15
+ export interface EntitySpec {
16
+ id: number;
17
+ teamId: number;
18
+ seed: number;
19
+ archetype: string;
20
+ weaponId: string;
21
+ armourId?: string;
22
+ x_m?: number;
23
+ y_m?: number;
24
+ }
25
+ /**
26
+ * Build a deterministic WorldState from a declarative entity spec list.
27
+ *
28
+ * - Uses spec.seed for generateIndividual() — no Math.random().
29
+ * - Position coordinates are fixed-point: Math.round(metres * SCALE.m).
30
+ * - Throws on unknown archetype, weaponId, or armourId.
31
+ * - Throws on duplicate entity ids.
32
+ */
33
+ export declare function createWorld(seed: number, entities: EntitySpec[]): WorldState;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * CE-2: createWorld convenience factory.
3
+ *
4
+ * Builds a deterministic WorldState from a simple declarative entity spec.
5
+ * No Math.random() — all randomness flows through generateIndividual(spec.seed, archetype).
6
+ * All position coordinates are fixed-point (SCALE.m multiplier + Math.round).
7
+ */
8
+ import { q, SCALE } from "./units.js";
9
+ import { generateIndividual } from "./generate.js";
10
+ import { HUMAN_BASE, AMATEUR_BOXER, PRO_BOXER, GRECO_WRESTLER, KNIGHT_INFANTRY, LARGE_PACIFIC_OCTOPUS, SERVICE_ROBOT, } from "./archetypes.js";
11
+ import { ELF_SPECIES, DWARF_SPECIES, HALFLING_SPECIES, ORC_SPECIES, OGRE_SPECIES, GOBLIN_SPECIES, TROLL_SPECIES, VULCAN_SPECIES, KLINGON_SPECIES, ROMULAN_SPECIES, DRAGON_SPECIES, CENTAUR_SPECIES, SATYR_SPECIES, HEECHEE_SPECIES, } from "./species.js";
12
+ import { ALL_HISTORICAL_MELEE, ALL_HISTORICAL_RANGED } from "./weapons.js";
13
+ import { STARTER_WEAPONS, STARTER_ARMOUR, STARTER_ARMOUR_11C } from "./equipment.js";
14
+ import { v3 } from "./sim/vec3.js";
15
+ import { defaultIntent } from "./sim/intent.js";
16
+ import { defaultAction } from "./sim/action.js";
17
+ import { defaultCondition } from "./sim/condition.js";
18
+ import { defaultInjury } from "./sim/injury.js";
19
+ // ── Static archetype map ──────────────────────────────────────────────────────
20
+ /** Map of string keys to Archetype objects for use with createWorld(). */
21
+ export const ARCHETYPE_MAP = new Map([
22
+ // Direct archetypes from archetypes.ts
23
+ ["HUMAN_BASE", HUMAN_BASE],
24
+ ["AMATEUR_BOXER", AMATEUR_BOXER],
25
+ ["PRO_BOXER", PRO_BOXER],
26
+ ["GRECO_WRESTLER", GRECO_WRESTLER],
27
+ ["KNIGHT_INFANTRY", KNIGHT_INFANTRY],
28
+ ["LARGE_PACIFIC_OCTOPUS", LARGE_PACIFIC_OCTOPUS],
29
+ ["SERVICE_ROBOT", SERVICE_ROBOT],
30
+ // Species archetypes from species.ts
31
+ ["ELF", ELF_SPECIES.archetype],
32
+ ["DWARF", DWARF_SPECIES.archetype],
33
+ ["HALFLING", HALFLING_SPECIES.archetype],
34
+ ["ORC", ORC_SPECIES.archetype],
35
+ ["OGRE", OGRE_SPECIES.archetype],
36
+ ["GOBLIN", GOBLIN_SPECIES.archetype],
37
+ ["TROLL", TROLL_SPECIES.archetype],
38
+ ["VULCAN", VULCAN_SPECIES.archetype],
39
+ ["KLINGON", KLINGON_SPECIES.archetype],
40
+ ["ROMULAN", ROMULAN_SPECIES.archetype],
41
+ ["DRAGON", DRAGON_SPECIES.archetype],
42
+ ["CENTAUR", CENTAUR_SPECIES.archetype],
43
+ ["SATYR", SATYR_SPECIES.archetype],
44
+ ["HEECHEE", HEECHEE_SPECIES.archetype],
45
+ ]);
46
+ // ── Static item map ───────────────────────────────────────────────────────────
47
+ function buildItemMap() {
48
+ const map = new Map();
49
+ const allItems = [
50
+ ...ALL_HISTORICAL_MELEE,
51
+ ...ALL_HISTORICAL_RANGED,
52
+ ...STARTER_WEAPONS,
53
+ ...STARTER_ARMOUR,
54
+ ...STARTER_ARMOUR_11C,
55
+ ];
56
+ for (const item of allItems) {
57
+ map.set(item.id, item);
58
+ }
59
+ return map;
60
+ }
61
+ /** Map of item id → Item for weapons and armour usable with createWorld(). */
62
+ export const ITEM_MAP = buildItemMap();
63
+ // ── createWorld ───────────────────────────────────────────────────────────────
64
+ /**
65
+ * Build a deterministic WorldState from a declarative entity spec list.
66
+ *
67
+ * - Uses spec.seed for generateIndividual() — no Math.random().
68
+ * - Position coordinates are fixed-point: Math.round(metres * SCALE.m).
69
+ * - Throws on unknown archetype, weaponId, or armourId.
70
+ * - Throws on duplicate entity ids.
71
+ */
72
+ export function createWorld(seed, entities) {
73
+ const built = [];
74
+ for (const spec of entities) {
75
+ // ── Archetype lookup ──────────────────────────────────────────────────────
76
+ const archetype = ARCHETYPE_MAP.get(spec.archetype);
77
+ if (archetype === undefined) {
78
+ throw new Error(`createWorld: unknown archetype "${spec.archetype}". ` +
79
+ `Valid keys: ${[...ARCHETYPE_MAP.keys()].join(", ")}`);
80
+ }
81
+ // ── Weapon lookup ─────────────────────────────────────────────────────────
82
+ const weapon = ITEM_MAP.get(spec.weaponId);
83
+ if (weapon === undefined) {
84
+ throw new Error(`createWorld: unknown weaponId "${spec.weaponId}"`);
85
+ }
86
+ // ── Optional armour lookup ────────────────────────────────────────────────
87
+ let armour;
88
+ if (spec.armourId !== undefined) {
89
+ armour = ITEM_MAP.get(spec.armourId);
90
+ if (armour === undefined) {
91
+ throw new Error(`createWorld: unknown armourId "${spec.armourId}"`);
92
+ }
93
+ }
94
+ // ── Generate individual attributes ────────────────────────────────────────
95
+ const attrs = generateIndividual(spec.seed, archetype);
96
+ // ── Default position ──────────────────────────────────────────────────────
97
+ // Team 1 defaults to x=0; all others default to x=0.6m.
98
+ const defaultX = spec.teamId === 1 ? 0 : 0.6;
99
+ const x_fixed = Math.round((spec.x_m ?? defaultX) * SCALE.m);
100
+ const y_fixed = Math.round((spec.y_m ?? 0) * SCALE.m);
101
+ // ── Build loadout ─────────────────────────────────────────────────────────
102
+ const items = [weapon];
103
+ if (armour !== undefined)
104
+ items.push(armour);
105
+ // ── Assemble entity ───────────────────────────────────────────────────────
106
+ const entity = {
107
+ id: spec.id,
108
+ teamId: spec.teamId,
109
+ attributes: attrs,
110
+ energy: { reserveEnergy_J: attrs.performance.reserveEnergy_J, fatigue: q(0) },
111
+ loadout: { items },
112
+ traits: [],
113
+ position_m: v3(x_fixed, y_fixed, 0),
114
+ velocity_mps: v3(0, 0, 0),
115
+ intent: defaultIntent(),
116
+ action: defaultAction(),
117
+ condition: defaultCondition(),
118
+ injury: defaultInjury(),
119
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0), position: "standing" },
120
+ };
121
+ built.push(entity);
122
+ }
123
+ // ── Sort by id ────────────────────────────────────────────────────────────
124
+ built.sort((a, b) => a.id - b.id);
125
+ // ── Duplicate id check ────────────────────────────────────────────────────
126
+ const ids = built.map(e => e.id);
127
+ const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
128
+ if (dupes.length > 0) {
129
+ throw new Error(`createWorld: duplicate entity IDs detected: ${[...new Set(dupes)].join(", ")}`);
130
+ }
131
+ return { tick: 0, seed, entities: built };
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@its-not-rocket-science/ananke",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
6
6
  "license": "MIT",