@polycode-projects/the-mechanical-code-talker 5.0.3 → 5.0.5
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/corpus/worlds/manifest.json +9 -9
- package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
- package/corpus/worlds/src/town-square-chapel.jsonl +1 -1
- package/corpus/worlds/src/town-square-market.jsonl +1 -1
- package/corpus/worlds/src/town-square.jsonl +1 -1
- package/data/mudiii-assets.json +11 -6
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +53 -0
- package/src/domain/game-config.mjs +9 -1
- package/src/domain/memory/causal-stability.mjs +82 -0
- package/src/domain/spider-fly-world.mjs +11 -13
- package/src/domain/town-square-world.mjs +36 -0
- package/src/services/mudiii-scene.mjs +131 -39
- package/src/services/mudiii-turn.mjs +332 -2
- package/src/services/mudiii-viz.mjs +76 -51
- package/src/services/predator-prey.mjs +68 -7
- package/src/services/spider-fly-viz.mjs +57 -60
- package/src/surfaces/web/memory-ask-browser.bundle.js +106 -106
- package/src/surfaces/web/mudiii-browser-entry.mjs +89 -5
|
@@ -50,17 +50,17 @@
|
|
|
50
50
|
{
|
|
51
51
|
"file": "src/town-square-chapel.jsonl",
|
|
52
52
|
"bytes": 108408,
|
|
53
|
-
"sha256": "
|
|
53
|
+
"sha256": "02036d284ec27244494e1f575b750efa71d9a0fc232e57dbd8b5dab7e6a6b7b3"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"file": "src/town-square-market.jsonl",
|
|
57
57
|
"bytes": 52311,
|
|
58
|
-
"sha256": "
|
|
58
|
+
"sha256": "dd374f9cb1c6a13ccfab62badeb4150b8cfefdc349f834430c2c9e105c1d7a4f"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"file": "src/town-square.jsonl",
|
|
62
62
|
"bytes": 75251,
|
|
63
|
-
"sha256": "
|
|
63
|
+
"sha256": "2fb87416759d8cee1464b55d4e811b91e772d52f65e0f06a88eac1d3a8fc5e7f"
|
|
64
64
|
}
|
|
65
65
|
],
|
|
66
66
|
"files": [
|
|
@@ -106,18 +106,18 @@
|
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"file": "shards/town-square-chapel.jsonl.gz",
|
|
109
|
-
"bytes":
|
|
110
|
-
"sha256": "
|
|
109
|
+
"bytes": 5538,
|
|
110
|
+
"sha256": "826e8732b419b506098f4fbce1adcc07d11bd7d91ea672aa46f13ed3ccebb0bb"
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"file": "shards/town-square-market.jsonl.gz",
|
|
114
|
-
"bytes":
|
|
115
|
-
"sha256": "
|
|
114
|
+
"bytes": 3113,
|
|
115
|
+
"sha256": "c6834f0754498ff800e7be620e3e2268c375686bd753fa9da9b7dd89a63aa009"
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
"file": "shards/town-square.jsonl.gz",
|
|
119
|
-
"bytes":
|
|
120
|
-
"sha256": "
|
|
119
|
+
"bytes": 4275,
|
|
120
|
+
"sha256": "460864ccee3a6eed9dae93f158abaf9780f2b7fb4051f45c3ebc63c7400b01d2"
|
|
121
121
|
}
|
|
122
122
|
]
|
|
123
123
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -920,7 +920,7 @@
|
|
|
920
920
|
{"world":"town-square-chapel","kind":"fact","subject":"goblin","predicate":"mgx:lose","object":"0.06 mass per turn by default"}
|
|
921
921
|
{"world":"town-square-chapel","kind":"fact","subject":"goblin","predicate":"mgx:arrive","object":"every 5 turns at the edge of the board by default"}
|
|
922
922
|
{"world":"town-square-chapel","kind":"fact","subject":"crumb","predicate":"mgx:arrive","object":"every 3 turns on any open cell by default"}
|
|
923
|
-
{"world":"town-square-chapel","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass
|
|
923
|
+
{"world":"town-square-chapel","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
924
924
|
{"world":"town-square-chapel","kind":"fact","subject":"morsel","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
925
925
|
{"world":"town-square-chapel","kind":"fact","subject":"morsel","predicate":"mgx:arrive","object":"where the player puts it"}
|
|
926
926
|
{"world":"town-square-chapel","kind":"fact","subject":"vision radius","predicate":"mgx:hasProperty","object":"4 cells for the fox by default"}
|
|
@@ -446,7 +446,7 @@
|
|
|
446
446
|
{"world":"town-square-market","kind":"fact","subject":"goblin","predicate":"mgx:lose","object":"0.06 mass per turn by default"}
|
|
447
447
|
{"world":"town-square-market","kind":"fact","subject":"goblin","predicate":"mgx:arrive","object":"every 5 turns at the edge of the board by default"}
|
|
448
448
|
{"world":"town-square-market","kind":"fact","subject":"crumb","predicate":"mgx:arrive","object":"every 3 turns on any open cell by default"}
|
|
449
|
-
{"world":"town-square-market","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass
|
|
449
|
+
{"world":"town-square-market","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
450
450
|
{"world":"town-square-market","kind":"fact","subject":"morsel","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
451
451
|
{"world":"town-square-market","kind":"fact","subject":"morsel","predicate":"mgx:arrive","object":"where the player puts it"}
|
|
452
452
|
{"world":"town-square-market","kind":"fact","subject":"vision radius","predicate":"mgx:hasProperty","object":"4 cells for the fox by default"}
|
|
@@ -678,7 +678,7 @@
|
|
|
678
678
|
{"world":"town-square","kind":"fact","subject":"goblin","predicate":"mgx:lose","object":"0.06 mass per turn by default"}
|
|
679
679
|
{"world":"town-square","kind":"fact","subject":"goblin","predicate":"mgx:arrive","object":"every 5 turns at the edge of the board by default"}
|
|
680
680
|
{"world":"town-square","kind":"fact","subject":"crumb","predicate":"mgx:arrive","object":"every 3 turns on any open cell by default"}
|
|
681
|
-
{"world":"town-square","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass
|
|
681
|
+
{"world":"town-square","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
682
682
|
{"world":"town-square","kind":"fact","subject":"morsel","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
683
683
|
{"world":"town-square","kind":"fact","subject":"morsel","predicate":"mgx:arrive","object":"where the player puts it"}
|
|
684
684
|
{"world":"town-square","kind":"fact","subject":"vision radius","predicate":"mgx:hasProperty","object":"4 cells for the fox by default"}
|
package/data/mudiii-assets.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"destPath": "public/models/creatures/fox.glb",
|
|
22
22
|
"licence": "CC0-1.0",
|
|
23
23
|
"author": "Quaternius",
|
|
24
|
-
"source": "https://poly.pizza/u/Quaternius
|
|
24
|
+
"source": "https://poly.pizza/u/Quaternius \u00b7 https://quaternius.com",
|
|
25
25
|
"bytes": 332748,
|
|
26
26
|
"sha256": "2694028b47e84bad3b9d985aa7743dae02a3bcfddd6becd73b46c747a1e067b3",
|
|
27
27
|
"targetHeight": 1.0,
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"walk": "Walk",
|
|
31
31
|
"run": "Gallop",
|
|
32
32
|
"attack": "Attack",
|
|
33
|
-
"hit": [
|
|
33
|
+
"hit": [
|
|
34
|
+
"Idle_HitReact_Left",
|
|
35
|
+
"Idle_HitReact_Right"
|
|
36
|
+
],
|
|
34
37
|
"death": "Death",
|
|
35
38
|
"eat": "Eating"
|
|
36
39
|
}
|
|
@@ -41,7 +44,7 @@
|
|
|
41
44
|
"destPath": "public/models/creatures/goblin.glb",
|
|
42
45
|
"licence": "CC0-1.0",
|
|
43
46
|
"author": "Quaternius",
|
|
44
|
-
"source": "https://poly.pizza/u/Quaternius
|
|
47
|
+
"source": "https://poly.pizza/u/Quaternius \u00b7 https://quaternius.com",
|
|
45
48
|
"bytes": 47924,
|
|
46
49
|
"sha256": "2f02ce927a8871554f3931655d75f1abb8237d36be8a19a35b09920d0812bc3c",
|
|
47
50
|
"targetHeight": 2.1,
|
|
@@ -50,7 +53,9 @@
|
|
|
50
53
|
"walk": "Walk",
|
|
51
54
|
"run": "Run",
|
|
52
55
|
"attack": "Attack",
|
|
53
|
-
"hit": [
|
|
56
|
+
"hit": [
|
|
57
|
+
"HitRecieve"
|
|
58
|
+
],
|
|
54
59
|
"death": "Death"
|
|
55
60
|
}
|
|
56
61
|
},
|
|
@@ -207,7 +212,7 @@
|
|
|
207
212
|
"source": "https://github.com/KayKit-Game-Assets/KayKit-Dungeon-Remastered-1.0",
|
|
208
213
|
"bytes": 14004,
|
|
209
214
|
"sha256": "8b340af18ea1325d5a9bd4a967d3ef37bb90838ca2d90927b123e23877f7ddad",
|
|
210
|
-
"targetHeight": 0.
|
|
215
|
+
"targetHeight": 0.6,
|
|
211
216
|
"clips": null
|
|
212
217
|
},
|
|
213
218
|
{
|
|
@@ -219,7 +224,7 @@
|
|
|
219
224
|
"source": "https://github.com/KayKit-Game-Assets/KayKit-Dungeon-Remastered-1.0",
|
|
220
225
|
"bytes": 14004,
|
|
221
226
|
"sha256": "8b340af18ea1325d5a9bd4a967d3ef37bb90838ca2d90927b123e23877f7ddad",
|
|
222
|
-
"targetHeight": 0.
|
|
227
|
+
"targetHeight": 0.6,
|
|
223
228
|
"clips": null
|
|
224
229
|
}
|
|
225
230
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
planRetraction, mergeRetractions, retractionFromWire, retractionWireFact,
|
|
51
51
|
isRetractedRecord, RETRACTION_CLASS,
|
|
52
52
|
} from "../../domain/memory/retraction.mjs";
|
|
53
|
+
import { admittedNodes, stableRecordIds } from "../../domain/memory/causal-stability.mjs";
|
|
53
54
|
import { assertIndividualValid } from "./shacl.mjs";
|
|
54
55
|
|
|
55
56
|
// The rollup vocabulary and its tuning constants live with the compaction
|
|
@@ -70,6 +71,8 @@ export {
|
|
|
70
71
|
retractedRecordIds, retractedAtOf, retractionWireFact, retractionFromWire,
|
|
71
72
|
} from "../../domain/memory/retraction.mjs";
|
|
72
73
|
|
|
74
|
+
export { admittedNodes, peersToConvince, stableRecordIds } from "../../domain/memory/causal-stability.mjs";
|
|
75
|
+
|
|
73
76
|
export const MEMORY_DIR_REL = join(".tmct", "memory");
|
|
74
77
|
export const MEMORY_GRAPH_REL = join(MEMORY_DIR_REL, "graph.json");
|
|
75
78
|
|
|
@@ -3189,6 +3192,56 @@ export async function appendRetractions(dir, wireFacts) {
|
|
|
3189
3192
|
return { merged: incoming.length, removed };
|
|
3190
3193
|
}
|
|
3191
3194
|
|
|
3195
|
+
/** What this store could retire, and the roster it has to convince first.
|
|
3196
|
+
*
|
|
3197
|
+
* `roster` is the world's admission graph, folded to a set of node ids —
|
|
3198
|
+
* replicated, grow-only, and the same on every peer holding the same facts.
|
|
3199
|
+
* `retirable` is the tombstones every peer on that roster is known to hold.
|
|
3200
|
+
* `acknowledgedBy(nodeId)` is what supplies that evidence; nothing produces it
|
|
3201
|
+
* yet, so `retirable` reads empty and this is a report rather than a sweep.
|
|
3202
|
+
* Retiring nothing is the current behaviour, and it is the safe one: a
|
|
3203
|
+
* tombstone dropped one peer early lets that peer's copy resurrect a retracted
|
|
3204
|
+
* fact. See docs/references/papers/crdt.md. */
|
|
3205
|
+
export function retirableRetractions(memory, { self = "", acknowledgedBy = null } = {}) {
|
|
3206
|
+
const roster = admittedNodes(readFactRows(memory));
|
|
3207
|
+
const recordIds = [];
|
|
3208
|
+
for (const ind of memory?.individuals || []) {
|
|
3209
|
+
if (ind?.class === RETRACTION_CLASS && ind.id) recordIds.push(ind.id);
|
|
3210
|
+
}
|
|
3211
|
+
return { roster, retirable: stableRecordIds({ recordIds, roster, self, acknowledgedBy }) };
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/** Drop named retraction records. Takes the ids rather than choosing them, so a
|
|
3215
|
+
* caller has to have run the stability rule and passed its answer; ids that are
|
|
3216
|
+
* not retraction records are skipped. Returns the ids that went. */
|
|
3217
|
+
export async function retireRetractions(dir, ids) {
|
|
3218
|
+
const asked = new Set((ids || []).filter(Boolean));
|
|
3219
|
+
const retired = [];
|
|
3220
|
+
if (!asked.size) return { retired };
|
|
3221
|
+
await mutateMemory(dir, (payload) => {
|
|
3222
|
+
const drop = new Set();
|
|
3223
|
+
payload.individuals = (payload.individuals || []).filter((ind) => {
|
|
3224
|
+
if (ind?.class !== RETRACTION_CLASS || !asked.has(ind.id)) return true;
|
|
3225
|
+
drop.add(ind.id);
|
|
3226
|
+
return false;
|
|
3227
|
+
});
|
|
3228
|
+
if (!drop.size) return;
|
|
3229
|
+
for (const id of drop) retired.push(id);
|
|
3230
|
+
const idx = memoryIndexOf(payload);
|
|
3231
|
+
if (idx) {
|
|
3232
|
+
for (const id of drop) {
|
|
3233
|
+
idx.individualsById.delete(id);
|
|
3234
|
+
const groupId = factGroupId(id);
|
|
3235
|
+
const held = (idx.retractionsByGroup.get(groupId) || []).filter((r) => !drop.has(r.id));
|
|
3236
|
+
if (held.length) idx.retractionsByGroup.set(groupId, held);
|
|
3237
|
+
else idx.retractionsByGroup.delete(groupId);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
recountClasses(payload);
|
|
3241
|
+
});
|
|
3242
|
+
return { retired };
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3192
3245
|
/** The trust floor a fact must clear before a differing object counts as a real
|
|
3193
3246
|
* contradiction (below it the fact is too weak to contradict anything). */
|
|
3194
3247
|
const CONTRADICTION_TRUST_FLOOR = 0.5;
|
|
@@ -78,7 +78,9 @@ export const DEFAULT_GAME_CONFIG = Object.freeze({
|
|
|
78
78
|
preyVisionRadius: 3,
|
|
79
79
|
preySpawnIntervalTurns: 5,
|
|
80
80
|
foodSpawnIntervalTurns: 3,
|
|
81
|
-
|
|
81
|
+
// Spawned and placed food are one thing: the same hay bale, worth the
|
|
82
|
+
// same. The two knobs stay separate so a world can still tell them apart.
|
|
83
|
+
spawnedFoodMass: 2,
|
|
82
84
|
placedFoodMass: 2,
|
|
83
85
|
maxPreyPopulation: 6,
|
|
84
86
|
maxFoodItems: 8,
|
|
@@ -88,6 +90,11 @@ export const DEFAULT_GAME_CONFIG = Object.freeze({
|
|
|
88
90
|
// this is the one switch that turns that off (visionRadius: Infinity for
|
|
89
91
|
// the food-only belief call) without any new belief machinery.
|
|
90
92
|
foodVisionGated: true,
|
|
93
|
+
// Whether the town-square lane accepts teaching. Its own knob rather than
|
|
94
|
+
// a share of the adventure one below: the board's sentence table is a
|
|
95
|
+
// different vocabulary, and a page checkbox on the town square should not
|
|
96
|
+
// have to set a key named after another surface.
|
|
97
|
+
teach: false,
|
|
91
98
|
}),
|
|
92
99
|
guessNumber: Object.freeze({
|
|
93
100
|
defaultLo: 1,
|
|
@@ -159,6 +166,7 @@ const MUDIII_KEY_MAP = Object.freeze({
|
|
|
159
166
|
max_prey_population: "maxPreyPopulation",
|
|
160
167
|
max_food_items: "maxFoodItems",
|
|
161
168
|
food_vision_gated: "foodVisionGated",
|
|
169
|
+
teach: "teach",
|
|
162
170
|
});
|
|
163
171
|
|
|
164
172
|
const ADVENTURE_KEY_MAP = Object.freeze({
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// memory/causal-stability.mjs — deciding when a replicated tombstone has been
|
|
2
|
+
// held by enough peers to retire.
|
|
3
|
+
//
|
|
4
|
+
// A retraction record and a compaction summary both work by staying put: they
|
|
5
|
+
// carry the ids they suppressed, and any peer that re-delivers one of those ids
|
|
6
|
+
// gets refused. That is what makes a delete survive a sync over a grow-only
|
|
7
|
+
// set. It also means the records accumulate, because nothing yet says when one
|
|
8
|
+
// has done its job.
|
|
9
|
+
//
|
|
10
|
+
// The literature calls the missing rule CAUSAL STABILITY: a record is safe to
|
|
11
|
+
// drop once every replica that could still send a conflicting copy has it. Two
|
|
12
|
+
// inputs, and the mesh has one of them.
|
|
13
|
+
//
|
|
14
|
+
// - The ROSTER. `node:<joiner> mgx:invitedBy node:<inviter>` is an ordinary
|
|
15
|
+
// replicated fact, so the set of node ids ever admitted to a world is a
|
|
16
|
+
// grow-only union every peer computes the same way. `admittedNodes` reads
|
|
17
|
+
// it. Grow-only is exactly right here: a roster that could shrink would let
|
|
18
|
+
// a forgotten node's stale copy back in.
|
|
19
|
+
// - The ACKNOWLEDGEMENT. Nothing yet records that a named node holds a named
|
|
20
|
+
// record. `stableRecordIds` takes it as an argument rather than inventing
|
|
21
|
+
// it, and answers "nothing is stable" when it is absent — which is the
|
|
22
|
+
// current answer, and the safe one.
|
|
23
|
+
//
|
|
24
|
+
// Every rule here errs the same way. Dropping a tombstone one peer short lets
|
|
25
|
+
// that peer's copy resurrect a retracted fact, and that failure is silent,
|
|
26
|
+
// late, and reads as the memory inventing something. Retiring nothing is
|
|
27
|
+
// merely unbounded. So: an empty roster retires nothing, a member with no
|
|
28
|
+
// acknowledgement retires nothing, and an unparseable input retires nothing.
|
|
29
|
+
//
|
|
30
|
+
// Pure: no clock, no counter, no arrival order. The answer is a function of the
|
|
31
|
+
// fact set and the acknowledgement evidence handed in, which is the same
|
|
32
|
+
// invariant every read-time resolver over the store has to meet.
|
|
33
|
+
// docs/references/papers/crdt.md carries the full design and the options it
|
|
34
|
+
// rejected.
|
|
35
|
+
import { INVITED_BY_PREDICATE } from "../p2p/facts.mjs";
|
|
36
|
+
|
|
37
|
+
/** The node ids a world has ever admitted, from its admission edges. Both ends
|
|
38
|
+
* of each edge count: the joiner wrote the edge about itself, and it names the
|
|
39
|
+
* node that let it in. Sorted, so two peers holding the same facts hand the
|
|
40
|
+
* same roster to the rule below. */
|
|
41
|
+
export function admittedNodes(rows) {
|
|
42
|
+
const nodes = new Set();
|
|
43
|
+
for (const row of rows || []) {
|
|
44
|
+
if (row?.predicate !== INVITED_BY_PREDICATE) continue;
|
|
45
|
+
if (row.subject) nodes.add(String(row.subject));
|
|
46
|
+
if (row.object) nodes.add(String(row.object));
|
|
47
|
+
}
|
|
48
|
+
return [...nodes].sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The roster this node has to convince before retiring anything: every
|
|
52
|
+
* admitted node except itself. A node holding its own record proves nothing
|
|
53
|
+
* about who else still has a copy. */
|
|
54
|
+
export function peersToConvince(roster, self = "") {
|
|
55
|
+
const me = String(self || "");
|
|
56
|
+
return (roster || []).map(String).filter((id) => id && id !== me);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Which of `recordIds` every peer on the roster is known to hold.
|
|
61
|
+
*
|
|
62
|
+
* `acknowledgedBy(nodeId)` returns the record ids that node is known to hold.
|
|
63
|
+
* Nothing supplies it in the product yet, so it defaults to knowing nothing and
|
|
64
|
+
* the answer defaults to the empty set. That default is the gate: this rule
|
|
65
|
+
* cannot retire anything until something can show a peer holds a record.
|
|
66
|
+
*
|
|
67
|
+
* An empty roster answers with nothing too. A store with no admission edges has
|
|
68
|
+
* not shown it is alone; it has shown it does not know who else is out there,
|
|
69
|
+
* and a copy of it can be sitting in a closed browser tab.
|
|
70
|
+
*/
|
|
71
|
+
export function stableRecordIds({ recordIds = [], roster = [], self = "", acknowledgedBy = null } = {}) {
|
|
72
|
+
const peers = peersToConvince(roster, self);
|
|
73
|
+
if (!peers.length) return [];
|
|
74
|
+
if (typeof acknowledgedBy !== "function") return [];
|
|
75
|
+
const held = new Map();
|
|
76
|
+
for (const peer of peers) {
|
|
77
|
+
const ids = acknowledgedBy(peer);
|
|
78
|
+
held.set(peer, new Set([...(ids || [])].map(String)));
|
|
79
|
+
}
|
|
80
|
+
const candidates = [...new Set((recordIds || []).map(String).filter(Boolean))].sort();
|
|
81
|
+
return candidates.filter((id) => peers.every((peer) => held.get(peer).has(id)));
|
|
82
|
+
}
|
|
@@ -57,9 +57,9 @@ export const chebyshevDistance = (ax, ay, bx, by) => Math.max(Math.abs(ax - bx),
|
|
|
57
57
|
export const inBounds = (x, y) => x >= 1 && x <= GRID_SIZE && y >= 1 && y <= GRID_SIZE;
|
|
58
58
|
|
|
59
59
|
/** Every cell within Chebyshev `radius` of (cx, cy), clipped to the board, in
|
|
60
|
-
* raster order — the one visibility primitive
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* raster order — the one visibility primitive ("visibleCells(cx, cy, radius=4)")
|
|
61
|
+
* shared by both agents' belief and this module's own web-block derivation
|
|
62
|
+
* below. */
|
|
63
63
|
export function visibleCells(cx, cy, radius) {
|
|
64
64
|
const out = [];
|
|
65
65
|
for (let y = Math.max(1, cy - radius); y <= Math.min(GRID_SIZE, cy + radius); y += 1) {
|
|
@@ -72,8 +72,7 @@ export function visibleCells(cx, cy, radius) {
|
|
|
72
72
|
|
|
73
73
|
export const isInWebBlock = (x, y) => chebyshevDistance(x, y, WEB_HOME.x, WEB_HOME.y) <= WEB_RADIUS;
|
|
74
74
|
|
|
75
|
-
/** Every board-edge (perimeter) cell, raster order
|
|
76
|
-
* arrives (PLAN_SPIDER_FLY.md §10). */
|
|
75
|
+
/** Every board-edge (perimeter) cell, raster order. */
|
|
77
76
|
export function perimeterCells() {
|
|
78
77
|
const out = [];
|
|
79
78
|
for (let y = 1; y <= GRID_SIZE; y += 1) {
|
|
@@ -110,11 +109,10 @@ export function oneStepDirectionBetween(fromCell, toCell) {
|
|
|
110
109
|
return null;
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
/** The world's seed taxonomy
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* as rdfs:subClassOf facts. */
|
|
112
|
+
/** The world's seed taxonomy — enough for the ontology-to-sprite worked example
|
|
113
|
+
* (a poodle sprite, a sheepdog falling back to the generic dog sprite) to run
|
|
114
|
+
* on the default persona without the --persona-size large flag. [subject,
|
|
115
|
+
* superclass] pairs, written as rdfs:subClassOf facts. */
|
|
118
116
|
export const SEED_TAXONOMY = Object.freeze([
|
|
119
117
|
Object.freeze(["poodle", "dog"]),
|
|
120
118
|
Object.freeze(["sheepdog", "dog"]),
|
|
@@ -293,9 +291,9 @@ export function isLiveRenderableAgent(id, state) {
|
|
|
293
291
|
/** A minimal, inert rule-row family, so scripts/build-worlds-pack.mjs's
|
|
294
292
|
* shared validator ("every world needs at least one rule row") passes.
|
|
295
293
|
* src/services/spider-fly-turn.mjs never reads these back: grid movement is
|
|
296
|
-
* hand-written pathfinding over findActionPath/findReachableSet
|
|
297
|
-
*
|
|
298
|
-
*
|
|
294
|
+
* hand-written pathfinding over findActionPath/findReachableSet, not the
|
|
295
|
+
* taught action-Rule DSL, so this rides in the shard unused, same as an
|
|
296
|
+
* unrelated fact would. */
|
|
299
297
|
export function* worldRuleRows() {
|
|
300
298
|
yield {
|
|
301
299
|
world: WORLD_NAME, kind: "rule", name: "go", ruleKind: "action-signature",
|
|
@@ -82,6 +82,42 @@ export function oneStepDirectionBetween(fromCell, toCell) {
|
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/** The eight points a facing may take, clockwise from north. The four
|
|
86
|
+
* cardinals are DIRECTION_DELTA's own keys, and they are the only ones a STEP
|
|
87
|
+
* can use — the grid has no diagonal exits. The four intercardinals are
|
|
88
|
+
* turn-only, which is what a forty-five degree turn on the spot writes. */
|
|
89
|
+
export const COMPASS_POINTS = Object.freeze([
|
|
90
|
+
"north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
/** `facing` turned `degrees` clockwise (a negative angle turns the other way),
|
|
94
|
+
* landing on one of COMPASS_POINTS. Null when `facing` isn't a compass point
|
|
95
|
+
* or the angle isn't a whole multiple of 45, so a caller refuses rather than
|
|
96
|
+
* rounding a nonsense turn into a real one. Pure. */
|
|
97
|
+
export function turnedFacing(facing, degrees) {
|
|
98
|
+
const at = COMPASS_POINTS.indexOf(String(facing ?? ""));
|
|
99
|
+
if (at < 0) return null;
|
|
100
|
+
if (!Number.isInteger(degrees) || degrees % 45 !== 0) return null;
|
|
101
|
+
const steps = degrees / 45;
|
|
102
|
+
return COMPASS_POINTS[(((at + steps) % 8) + 8) % 8];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The compass point directly behind `facing`, or null. Pure. */
|
|
106
|
+
export function reverseFacing(facing) {
|
|
107
|
+
return turnedFacing(facing, 180);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The cell one step `direction` from `cell`, or null when `direction` names
|
|
111
|
+
* no cardinal step or `cell` doesn't parse. Bounds and props are NOT checked
|
|
112
|
+
* here: whether that cell can actually be entered is the exit table's answer,
|
|
113
|
+
* and there is only one of those. Pure. */
|
|
114
|
+
export function stepCellFrom(cell, direction) {
|
|
115
|
+
const at = parseCellId(cell);
|
|
116
|
+
const delta = DIRECTION_DELTA[String(direction ?? "")];
|
|
117
|
+
if (!at || !delta) return null;
|
|
118
|
+
return cellId(at.x + delta.dx, at.y + delta.dy);
|
|
119
|
+
}
|
|
120
|
+
|
|
85
121
|
// ---- the prop vocabulary ------------------------------------------------------
|
|
86
122
|
|
|
87
123
|
/** The closed set of noun stems a prop id may use ("house-1" -> "house"). A
|