@polycode-projects/the-mechanical-code-talker 5.0.2 → 5.0.4
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 +35 -5
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +236 -18
- package/src/domain/ask-vocab.mjs +1 -1
- package/src/domain/ask.mjs +20 -10
- package/src/domain/game-config.mjs +10 -1
- package/src/domain/interpret/normalize.mjs +4 -0
- package/src/domain/memory/retraction.mjs +232 -0
- package/src/domain/p2p/sync-filter.mjs +9 -1
- package/src/domain/spider-fly-world.mjs +80 -35
- package/src/domain/syllogise.mjs +128 -75
- package/src/services/adventure-autoplay.mjs +2 -2
- package/src/services/adventure-viz.mjs +12 -7
- package/src/services/chat.mjs +42 -14
- package/src/services/mud-turn.mjs +1 -1
- package/src/services/mud-viz.mjs +11 -2
- package/src/services/mudiii-scene.mjs +199 -55
- package/src/services/mudiii-turn.mjs +82 -1
- package/src/services/mudiii-viz.mjs +17 -7
- package/src/services/p2p-room.mjs +130 -9
- package/src/services/predator-prey.mjs +524 -69
- package/src/services/spider-fly-turn.mjs +128 -56
- package/src/services/spider-fly-viz.mjs +65 -71
- package/src/services/world-teach.mjs +3 -3
- package/src/surfaces/web/adventure-browser-entry.mjs +12 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +118 -118
- package/src/surfaces/web/mud-browser-entry.mjs +10 -3
- package/src/surfaces/web/mudiii-browser-entry.mjs +3 -3
- package/src/surfaces/web/spider-fly-browser-entry.mjs +26 -22
- package/src/services/spider-fly.mjs +0 -943
|
@@ -78,10 +78,18 @@ 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,
|
|
87
|
+
// Whether food only enters an agent's belief once it is within vision
|
|
88
|
+
// radius, the same as every other candidate. True by default — a crumb
|
|
89
|
+
// known to everyone the instant it spawns changes the foraging feel, and
|
|
90
|
+
// this is the one switch that turns that off (visionRadius: Infinity for
|
|
91
|
+
// the food-only belief call) without any new belief machinery.
|
|
92
|
+
foodVisionGated: true,
|
|
85
93
|
}),
|
|
86
94
|
guessNumber: Object.freeze({
|
|
87
95
|
defaultLo: 1,
|
|
@@ -152,6 +160,7 @@ const MUDIII_KEY_MAP = Object.freeze({
|
|
|
152
160
|
placed_food_mass: "placedFoodMass",
|
|
153
161
|
max_prey_population: "maxPreyPopulation",
|
|
154
162
|
max_food_items: "maxFoodItems",
|
|
163
|
+
food_vision_gated: "foodVisionGated",
|
|
155
164
|
});
|
|
156
165
|
|
|
157
166
|
const ADVENTURE_KEY_MAP = Object.freeze({
|
|
@@ -123,6 +123,10 @@ export function correctMisspellings(text) {
|
|
|
123
123
|
return String(text || "").replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
127
|
+
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
128
|
+
export const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
129
|
+
|
|
126
130
|
const FILLER_RE = FILLER_WORDS.length
|
|
127
131
|
? new RegExp(
|
|
128
132
|
"\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b\\s*,?",
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// memory/retraction.mjs — making a retraction survive the next sync.
|
|
2
|
+
//
|
|
3
|
+
// removeFacts is a real delete, and deleting from a replicated grow-only set is
|
|
4
|
+
// not a G-Set operation: a peer that still holds the fact re-sends it and the
|
|
5
|
+
// retraction is undone. What closes that hole is the move compaction's chain
|
|
6
|
+
// summary already makes. The retraction leaves a RECORD behind, the record
|
|
7
|
+
// carries the record ids it suppressed, and two records at one id merge by
|
|
8
|
+
// UNION of those ids. Union is a join, so two peers that retracted at different
|
|
9
|
+
// moments converge, and merging twice changes nothing.
|
|
10
|
+
//
|
|
11
|
+
// A retraction suppresses ONE SOURCE'S assertion, not the whole triple group.
|
|
12
|
+
// Two peers who independently taught the same fact hold two records; one of
|
|
13
|
+
// them retracting leaves the fact standing and cited to the other, and the
|
|
14
|
+
// retraction stays on record rather than erasing what was asserted. That falls
|
|
15
|
+
// out of absorbing concrete record ids: a record another peer holds and this
|
|
16
|
+
// one never did is not in the set.
|
|
17
|
+
//
|
|
18
|
+
// The instant matters as much as the ids. One source asserting the same triple
|
|
19
|
+
// again lands on the SAME content address, so an id on its own cannot tell a
|
|
20
|
+
// suppressed assertion from a later, deliberate one. The record therefore
|
|
21
|
+
// carries the moment of the retraction, and both enforcement points compare an
|
|
22
|
+
// assertion's own time against it: at or before, suppressed; after, it stands.
|
|
23
|
+
// Max is a join too, so that field merges in either order and agrees.
|
|
24
|
+
//
|
|
25
|
+
// Pure: this module plans, merges and encodes retraction RECORDS. core.mjs owns
|
|
26
|
+
// the store, p2p-room.mjs owns the wire. Its own class, its own id suffix and
|
|
27
|
+
// its own predicate keep it clear of compaction — a compacted record and a
|
|
28
|
+
// retracted one mean opposite things, and one namespace would let a summary
|
|
29
|
+
// read as a tombstone. The CRDT vocabulary above is pinned in
|
|
30
|
+
// docs/references/papers/crdt.md.
|
|
31
|
+
|
|
32
|
+
export const RETRACTION_CLASS = "Retraction";
|
|
33
|
+
|
|
34
|
+
/** The predicate a retraction travels under. The sync filters admit it on its
|
|
35
|
+
* own, because a retraction has to cross a wire that a chat room gates on
|
|
36
|
+
* provenance kind and a mud room gates on world predicates. */
|
|
37
|
+
export const RETRACTION_PREDICATE = "mgx:retracted";
|
|
38
|
+
|
|
39
|
+
export const RETRACTED_RECORD_IDS_PROP = "mgx:retractedRecordIds";
|
|
40
|
+
export const RETRACTED_AT_PROP = "mgx:retractedAt";
|
|
41
|
+
export const RETRACTED_COUNT_PROP = "mgx:retractedCount";
|
|
42
|
+
|
|
43
|
+
const RETRACTION_SUFFIX = "#retracted";
|
|
44
|
+
const PROVENANCE_PROP = "mgx:factProvenance";
|
|
45
|
+
const NO_INSTANT = "-";
|
|
46
|
+
|
|
47
|
+
/** One retraction record per (triple, source), the same per-source shape
|
|
48
|
+
* compaction's chain summary keys on. */
|
|
49
|
+
export const retractionIdFor = (groupId, sourceId) => `${groupId}@${sourceId}${RETRACTION_SUFFIX}`;
|
|
50
|
+
export const isRetractionId = (id) => String(id || "").endsWith(RETRACTION_SUFFIX);
|
|
51
|
+
|
|
52
|
+
/** The record id a retraction suppresses, read back off its own id. */
|
|
53
|
+
export function retractionScopeOf(retractionId) {
|
|
54
|
+
const id = String(retractionId || "");
|
|
55
|
+
return isRetractionId(id) ? id.slice(0, -RETRACTION_SUFFIX.length) : "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const attrValue = (ind, prop) => (ind?.attributes || []).find((a) => a?.prop === prop)?.value || "";
|
|
59
|
+
const idList = (value) => String(value || "").split(" ").filter(Boolean);
|
|
60
|
+
const tagList = (value) => String(value || "").split(" | ").filter(Boolean);
|
|
61
|
+
|
|
62
|
+
export const retractedRecordIds = (ind) => idList(attrValue(ind, RETRACTED_RECORD_IDS_PROP));
|
|
63
|
+
export const retractedAtOf = (ind) => attrValue(ind, RETRACTED_AT_PROP);
|
|
64
|
+
|
|
65
|
+
/** The later of two instants, tolerating "" and unparseable input on either
|
|
66
|
+
* side. Max is a join, which is what lets two records merge in either order. */
|
|
67
|
+
function laterOf(a, b) {
|
|
68
|
+
const at = Date.parse(a);
|
|
69
|
+
const bt = Date.parse(b);
|
|
70
|
+
if (!Number.isFinite(at)) return Number.isFinite(bt) ? String(b) : "";
|
|
71
|
+
if (!Number.isFinite(bt)) return String(a);
|
|
72
|
+
return bt > at ? String(b) : String(a);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const templateOf = (record) => ({
|
|
76
|
+
label: record?.label || "",
|
|
77
|
+
subject: attrValue(record, "rdf:subject"),
|
|
78
|
+
predicate: attrValue(record, "rdf:predicate"),
|
|
79
|
+
object: attrValue(record, "rdf:object"),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build a retraction record from the only things that define one: which record
|
|
84
|
+
* ids it suppressed, when, and who said so. Planning and merging both come
|
|
85
|
+
* through here, so a record built by retracting and the same record reached by
|
|
86
|
+
* merging two halves come out identical — the property the whole design rests
|
|
87
|
+
* on. The triple it names is carried for a reader; nothing enforces on it.
|
|
88
|
+
*/
|
|
89
|
+
function buildRetraction({ id, sourceId, template, ids, retractedAt, tags }) {
|
|
90
|
+
const sorted = [...new Set(ids)].filter(Boolean).sort();
|
|
91
|
+
const provenance = [...new Set(tags)].filter(Boolean).sort();
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
label: template?.label || "",
|
|
95
|
+
class: RETRACTION_CLASS,
|
|
96
|
+
derived_from: [],
|
|
97
|
+
mentions: [],
|
|
98
|
+
attributes: [
|
|
99
|
+
{ prop: "rdf:subject", key: "subject", value: template?.subject || "" },
|
|
100
|
+
{ prop: "rdf:predicate", key: "predicate", value: template?.predicate || "" },
|
|
101
|
+
{ prop: "rdf:object", key: "object", value: template?.object || "" },
|
|
102
|
+
{ prop: "mgx:sourceId", key: "sourceId", value: sourceId || "" },
|
|
103
|
+
{ prop: RETRACTED_RECORD_IDS_PROP, key: "retractedRecordIds", value: sorted.join(" ") },
|
|
104
|
+
{ prop: RETRACTED_COUNT_PROP, key: "retractedCount", value: String(sorted.length) },
|
|
105
|
+
{ prop: RETRACTED_AT_PROP, key: "retractedAt", value: retractedAt || "" },
|
|
106
|
+
...(provenance.length ? [{ prop: PROVENANCE_PROP, key: "provenance", value: provenance.join(" | ") }] : []),
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Plan the record one source's retraction leaves behind. `recordIds` are the
|
|
113
|
+
* ids actually removed for that source — its head, its demoted leaves, and any
|
|
114
|
+
* summary standing for them. `existing` is the record this source already has
|
|
115
|
+
* here, if the triple has been retracted before; its ids come along, so
|
|
116
|
+
* retracting twice keeps one record rather than growing a chain of them.
|
|
117
|
+
*
|
|
118
|
+
* Null when there is nothing to suppress, which keeps a no-op removal from
|
|
119
|
+
* writing a tombstone for a fact nobody ever asserted.
|
|
120
|
+
*/
|
|
121
|
+
export function planRetraction({ groupId, sourceId, recordIds = [], retractedAt = "", existing = null, template = null, provenance = "" }) {
|
|
122
|
+
if (!groupId || !sourceId) return null;
|
|
123
|
+
const ids = [...retractedRecordIds(existing), ...recordIds].filter(Boolean);
|
|
124
|
+
if (!ids.length) return null;
|
|
125
|
+
return buildRetraction({
|
|
126
|
+
id: retractionIdFor(groupId, sourceId),
|
|
127
|
+
sourceId,
|
|
128
|
+
template: template || templateOf(existing),
|
|
129
|
+
ids,
|
|
130
|
+
retractedAt: laterOf(retractedAtOf(existing), retractedAt),
|
|
131
|
+
tags: [...tagList(attrValue(existing, PROVENANCE_PROP)), ...tagList(provenance)],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Join two retraction records that share an id: union the suppressed ids and
|
|
137
|
+
* the tags, take the later instant, then re-derive everything else from that.
|
|
138
|
+
* Commutative, associative and idempotent, because union and max both are and
|
|
139
|
+
* because the count is a function of the union rather than a separate running
|
|
140
|
+
* total.
|
|
141
|
+
*/
|
|
142
|
+
export function mergeRetractions(existing, incoming) {
|
|
143
|
+
const template = templateOf(attrValue(existing, "rdf:subject") ? existing : incoming);
|
|
144
|
+
return buildRetraction({
|
|
145
|
+
id: existing?.id || incoming?.id,
|
|
146
|
+
sourceId: attrValue(existing, "mgx:sourceId") || attrValue(incoming, "mgx:sourceId"),
|
|
147
|
+
template,
|
|
148
|
+
ids: [...retractedRecordIds(existing), ...retractedRecordIds(incoming)],
|
|
149
|
+
retractedAt: laterOf(retractedAtOf(existing), retractedAtOf(incoming)),
|
|
150
|
+
tags: [...tagList(attrValue(existing, PROVENANCE_PROP)), ...tagList(attrValue(incoming, PROVENANCE_PROP))],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Whether an assertion made at `assertedAt` is old enough for the retraction
|
|
155
|
+
* to bite. An assertion with no readable time cannot show it is newer, so it
|
|
156
|
+
* is suppressed; the same goes for a record whose own instant will not parse.
|
|
157
|
+
* Erring this way keeps a resurrected copy out, and a source that means to say
|
|
158
|
+
* the thing again says it with a fresh tag. */
|
|
159
|
+
function notLaterThan(assertedAt, retractedAt) {
|
|
160
|
+
const asserted = Date.parse(assertedAt);
|
|
161
|
+
const retracted = Date.parse(retractedAt);
|
|
162
|
+
if (!Number.isFinite(retracted) || !Number.isFinite(asserted)) return true;
|
|
163
|
+
return asserted <= retracted;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Does this record suppress `recordId` as asserted at `assertedAt`? */
|
|
167
|
+
export function suppressesRecord(retraction, recordId, assertedAt = "") {
|
|
168
|
+
if (!retraction || !recordId) return false;
|
|
169
|
+
if (!retractedRecordIds(retraction).includes(recordId)) return false;
|
|
170
|
+
return notLaterThan(assertedAt, retractedAtOf(retraction));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The same check across a group's records. Both enforcement halves — the strip
|
|
174
|
+
* on read and the refusal on ingest — ask exactly this question. */
|
|
175
|
+
export function isRetractedRecord(retractions, recordId, assertedAt = "") {
|
|
176
|
+
for (const retraction of retractions || []) {
|
|
177
|
+
if (suppressesRecord(retraction, recordId, assertedAt)) return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The instant and the ids packed into one triple slot. The instant leads, ids
|
|
183
|
+
* follow, space separated — neither an ISO instant nor a record id carries a
|
|
184
|
+
* space, so the split is unambiguous. */
|
|
185
|
+
export function encodeRetractionValue(retractedAt, ids) {
|
|
186
|
+
const sorted = [...new Set(ids || [])].filter(Boolean).sort();
|
|
187
|
+
return [retractedAt || NO_INSTANT, ...sorted].join(" ");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function decodeRetractionValue(value) {
|
|
191
|
+
const parts = String(value || "").split(" ").filter(Boolean);
|
|
192
|
+
const head = parts[0] || "";
|
|
193
|
+
const retractedAt = head === NO_INSTANT || !Number.isFinite(Date.parse(head)) ? "" : head;
|
|
194
|
+
const ids = retractedAt || head === NO_INSTANT ? parts.slice(1) : parts;
|
|
195
|
+
return { retractedAt, ids };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A stored record as the one wire fact that carries it: the record id it
|
|
199
|
+
* suppresses as the subject, the retraction predicate, and the instant plus
|
|
200
|
+
* the suppressed ids as the object. Everything else on the record is derived
|
|
201
|
+
* from those, so the round trip loses nothing a peer enforces on. */
|
|
202
|
+
export function retractionWireFact(record) {
|
|
203
|
+
const scope = retractionScopeOf(record?.id);
|
|
204
|
+
if (!scope) return null;
|
|
205
|
+
return {
|
|
206
|
+
id: record.id,
|
|
207
|
+
subject: scope,
|
|
208
|
+
predicate: RETRACTION_PREDICATE,
|
|
209
|
+
object: encodeRetractionValue(retractedAtOf(record), retractedRecordIds(record)),
|
|
210
|
+
provenance: attrValue(record, PROVENANCE_PROP),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The other direction: a received wire fact as the record it stands for.
|
|
215
|
+
* Null for anything that is not a well-formed retraction, so a malformed
|
|
216
|
+
* message is dropped rather than merged. */
|
|
217
|
+
export function retractionFromWire(fact) {
|
|
218
|
+
if (!fact || fact.predicate !== RETRACTION_PREDICATE) return null;
|
|
219
|
+
const scope = String(fact.subject || "");
|
|
220
|
+
const at = scope.indexOf("@");
|
|
221
|
+
if (at <= 0 || at === scope.length - 1) return null;
|
|
222
|
+
const { retractedAt, ids } = decodeRetractionValue(fact.object);
|
|
223
|
+
if (!ids.length) return null;
|
|
224
|
+
return buildRetraction({
|
|
225
|
+
id: `${scope}${RETRACTION_SUFFIX}`,
|
|
226
|
+
sourceId: scope.slice(at + 1),
|
|
227
|
+
template: null,
|
|
228
|
+
ids,
|
|
229
|
+
retractedAt,
|
|
230
|
+
tags: tagList(fact.provenance),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
// What actually needs syncing is the delta: whatever a person or a peer
|
|
6
6
|
// actually added since boot.
|
|
7
7
|
import { provenanceTagToSource } from "../memory/trust.mjs";
|
|
8
|
+
import { RETRACTION_PREDICATE } from "../memory/retraction.mjs";
|
|
9
|
+
|
|
10
|
+
// A retraction crosses on both surfaces, whatever else they disagree about.
|
|
11
|
+
// It carries no teach tag of its own to key on and no world predicate, so
|
|
12
|
+
// leaving it to either filter's own rule would strand it and the deleted fact
|
|
13
|
+
// would come straight back from the next peer that still holds it.
|
|
14
|
+
const alwaysSyncable = (row) => row?.predicate === RETRACTION_PREDICATE;
|
|
8
15
|
|
|
9
16
|
// "teachNode" is the same human teaching, seen from one hop further out: a
|
|
10
17
|
// peer's own relabeled tag, keyed on the node id it carries. It syncs for
|
|
@@ -16,6 +23,7 @@ const CHAT_SYNCABLE_KINDS = new Set(["teach", "operator", "teachNode"]);
|
|
|
16
23
|
* asserted — never a row from the shipped corpus. */
|
|
17
24
|
export function chatSyncableFacts(rows) {
|
|
18
25
|
return rows.filter((row) => {
|
|
26
|
+
if (alwaysSyncable(row)) return true;
|
|
19
27
|
const source = provenanceTagToSource(row.provenance);
|
|
20
28
|
return source ? CHAT_SYNCABLE_KINDS.has(source.kind) : false;
|
|
21
29
|
});
|
|
@@ -31,5 +39,5 @@ export function chatSyncableFacts(rows) {
|
|
|
31
39
|
* this module's own P2P predicates via `extraPredicates`. */
|
|
32
40
|
export function mudSyncableFacts(rows, isMudStatePredicate, extraPredicates = []) {
|
|
33
41
|
const extra = new Set(extraPredicates);
|
|
34
|
-
return rows.filter((row) => extra.has(row.predicate) || isMudStatePredicate(row.predicate));
|
|
42
|
+
return rows.filter((row) => alwaysSyncable(row) || extra.has(row.predicate) || isMudStatePredicate(row.predicate));
|
|
35
43
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// structural self-description facts below quote the same shipped defaults
|
|
6
6
|
// the engine actually runs. Both
|
|
7
7
|
// scripts/gen-spider-fly-world.mjs (writes corpus/worlds/src/spider-fly.jsonl)
|
|
8
|
-
// and src/services/spider-fly.mjs (the runtime) read the SAME grid/web
|
|
8
|
+
// and src/services/spider-fly-turn.mjs (the runtime) read the SAME grid/web
|
|
9
9
|
// constants from here, so the shipped world and the engine that plays it can
|
|
10
10
|
// never drift apart.
|
|
11
11
|
|
|
@@ -14,28 +14,34 @@ import { DEFAULT_GAME_CONFIG } from "./game-config.mjs";
|
|
|
14
14
|
export const WORLD_NAME = "spider-fly";
|
|
15
15
|
export const GRID_SIZE = 10;
|
|
16
16
|
|
|
17
|
-
// The spider's home cell, and the web's Chebyshev radius around it (radius 1
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// The spider's home cell, and the web's Chebyshev radius around it (radius 1 =
|
|
18
|
+
// a 3x3 block). Corner-ish on purpose: a spider that starts here sees close to
|
|
19
|
+
// half the board just from edge-clipping.
|
|
20
20
|
export const WEB_HOME = Object.freeze({ x: 2, y: 2 });
|
|
21
21
|
export const WEB_RADIUS = 1;
|
|
22
22
|
|
|
23
|
-
// A spider-built
|
|
24
|
-
//
|
|
25
|
-
// static home zone's own always-on web without needing separate code paths.
|
|
23
|
+
// A spider-built web stays active for this many turns past the turn it was
|
|
24
|
+
// spun, mirroring the home zone's own always-on web without a second code path.
|
|
26
25
|
export const WEB_DURATION_TURNS = 10;
|
|
27
26
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// (
|
|
34
|
-
// tunable here — overridable per session via tmct.toml's [games.spider-fly]
|
|
35
|
-
// (src/domain/game-config.mjs).
|
|
27
|
+
// A spider starves like a fly does, and gains exactly a fly's remaining mass on
|
|
28
|
+
// an eat. Heavier starting mass than a single fly's worth on purpose — a spider
|
|
29
|
+
// that eats nothing for a while has some runway before starving. The decrement
|
|
30
|
+
// is half a fly's own (spiders live longer between meals than flies do), and —
|
|
31
|
+
// like every other tunable here — overridable per session via tmct.toml's
|
|
32
|
+
// [games.spider-fly] (src/domain/game-config.mjs).
|
|
36
33
|
export const SPIDER_INITIAL_MASS = 15;
|
|
37
34
|
export const SPIDER_MASS_DECREMENT_PER_TURN = 0.5;
|
|
38
35
|
|
|
36
|
+
/** The cast the shared predator/prey engine runs this board with. Same shape as
|
|
37
|
+
* the town square's own roles object, minus the food entry: nothing inert
|
|
38
|
+
* lies on a spider-and-fly board, and a null food role is what says so. */
|
|
39
|
+
export const SPIDER_FLY_ROLES = Object.freeze({
|
|
40
|
+
predator: Object.freeze({ role: "predator", kind: "spider", idPrefix: "spider" }),
|
|
41
|
+
prey: Object.freeze({ role: "prey", kind: "fly", idPrefix: "fly" }),
|
|
42
|
+
food: null,
|
|
43
|
+
});
|
|
44
|
+
|
|
39
45
|
export const cellId = (x, y) => `cell-${x}-${y}`;
|
|
40
46
|
|
|
41
47
|
const CELL_ID_RE = /^cell-(\d+)-(\d+)$/;
|
|
@@ -51,9 +57,9 @@ export const chebyshevDistance = (ax, ay, bx, by) => Math.max(Math.abs(ax - bx),
|
|
|
51
57
|
export const inBounds = (x, y) => x >= 1 && x <= GRID_SIZE && y >= 1 && y <= GRID_SIZE;
|
|
52
58
|
|
|
53
59
|
/** Every cell within Chebyshev `radius` of (cx, cy), clipped to the board, in
|
|
54
|
-
* raster order — the one visibility primitive
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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. */
|
|
57
63
|
export function visibleCells(cx, cy, radius) {
|
|
58
64
|
const out = [];
|
|
59
65
|
for (let y = Math.max(1, cy - radius); y <= Math.min(GRID_SIZE, cy + radius); y += 1) {
|
|
@@ -66,8 +72,7 @@ export function visibleCells(cx, cy, radius) {
|
|
|
66
72
|
|
|
67
73
|
export const isInWebBlock = (x, y) => chebyshevDistance(x, y, WEB_HOME.x, WEB_HOME.y) <= WEB_RADIUS;
|
|
68
74
|
|
|
69
|
-
/** Every board-edge (perimeter) cell, raster order
|
|
70
|
-
* arrives (PLAN_SPIDER_FLY.md §10). */
|
|
75
|
+
/** Every board-edge (perimeter) cell, raster order. */
|
|
71
76
|
export function perimeterCells() {
|
|
72
77
|
const out = [];
|
|
73
78
|
for (let y = 1; y <= GRID_SIZE; y += 1) {
|
|
@@ -94,11 +99,9 @@ export const DIRECTION_DELTA = Object.freeze({
|
|
|
94
99
|
* sits EXACTLY one cardinal step away (DIRECTION_DELTA) — null for the same
|
|
95
100
|
* cell, a diagonal, or any multi-step gap, so a caller never overstates
|
|
96
101
|
* "adjacent". The one shared primitive both the engine's own plan-driven
|
|
97
|
-
* facing
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* the chat-turn layer to get it (spider-fly-turn.mjs already imports
|
|
101
|
-
* spider-fly.mjs; the reverse would cycle). */
|
|
102
|
+
* facing and the chat dock's deception pills (spider-fly-turn.mjs's
|
|
103
|
+
* pillsForSpiderFly) need — defined once here so neither has to re-derive
|
|
104
|
+
* it. */
|
|
102
105
|
export function oneStepDirectionBetween(fromCell, toCell) {
|
|
103
106
|
for (const [direction, { dx, dy }] of Object.entries(DIRECTION_DELTA)) {
|
|
104
107
|
if (fromCell.x + dx === toCell.x && fromCell.y + dy === toCell.y) return direction;
|
|
@@ -106,11 +109,10 @@ export function oneStepDirectionBetween(fromCell, toCell) {
|
|
|
106
109
|
return null;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
/** The world's seed taxonomy
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* 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. */
|
|
114
116
|
export const SEED_TAXONOMY = Object.freeze([
|
|
115
117
|
Object.freeze(["poodle", "dog"]),
|
|
116
118
|
Object.freeze(["sheepdog", "dog"]),
|
|
@@ -124,13 +126,56 @@ export const SEED_TAXONOMY = Object.freeze([
|
|
|
124
126
|
export const WORLD_OPENING =
|
|
125
127
|
"a spider waits in its web; a fly drifts in from the edge of the board. Neither is yours to move. Watch, or address one by name in chat.";
|
|
126
128
|
|
|
129
|
+
/** This board as the shared predator/prey engine's own layout: a bare 10x10
|
|
130
|
+
* grid, the always-on web block, and the cast a fresh session mints. `props`
|
|
131
|
+
* is empty and stays empty — nothing here blocks movement, so every cell is
|
|
132
|
+
* open and the whole perimeter takes an arrival. */
|
|
133
|
+
export const SPIDER_FLY_LAYOUT = Object.freeze({
|
|
134
|
+
name: WORLD_NAME,
|
|
135
|
+
gridSize: GRID_SIZE,
|
|
136
|
+
opening: WORLD_OPENING,
|
|
137
|
+
boardNoun: "board",
|
|
138
|
+
boardSubject: "board",
|
|
139
|
+
cast: Object.freeze({ predators: 1, prey: 1 }),
|
|
140
|
+
props: Object.freeze([]),
|
|
141
|
+
staticWebAt: isInWebBlock,
|
|
142
|
+
webHomeCell: cellId(WEB_HOME.x, WEB_HOME.y),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/** The spider-and-fly knobs restated in the engine's role-keyed shape, with the
|
|
146
|
+
* three mechanics this cast wants switched on. The public [games.spider-fly]
|
|
147
|
+
* table stays species-keyed on purpose: a tmct.toml key and a page slider keep
|
|
148
|
+
* the names a player of THIS game would use, and the translation lives here.
|
|
149
|
+
* Unset keys fall back to the shipped defaults, so a partial slider payload is
|
|
150
|
+
* always a complete engine config. Pure. */
|
|
151
|
+
export function spiderFlyEngineConfig(knobs) {
|
|
152
|
+
const k = { ...DEFAULT_GAME_CONFIG.spiderFly, ...(knobs || {}) };
|
|
153
|
+
return {
|
|
154
|
+
predatorInitialMass: k.spiderInitialMass,
|
|
155
|
+
predatorMassDecrementPerTurn: k.spiderMassDecrementPerTurn,
|
|
156
|
+
predatorVisionRadius: k.spiderVisionRadius,
|
|
157
|
+
preyInitialMass: k.flyInitialMass,
|
|
158
|
+
preyMassDecrementPerTurn: k.flyMassDecrementPerTurn,
|
|
159
|
+
preyVisionRadius: k.flyVisionRadius,
|
|
160
|
+
preySpawnIntervalTurns: k.flySpawnIntervalTurns,
|
|
161
|
+
webDurationTurns: k.webDurationTurns,
|
|
162
|
+
eggLayMassThreshold: k.eggLayMassThreshold,
|
|
163
|
+
eggHatchDelayTurns: k.eggHatchDelayTurns,
|
|
164
|
+
eggHatchCount: k.eggHatchCount,
|
|
165
|
+
minHatchlingMass: k.minHatchlingMass,
|
|
166
|
+
carryPreyToWeb: true,
|
|
167
|
+
buildWebs: true,
|
|
168
|
+
layEggs: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
127
172
|
/** Every fact row the shipped world source carries: cell typing, grid
|
|
128
173
|
* adjacency (mgx:has-exit-<direction>), the web block (mgx:in-web) and the
|
|
129
174
|
* seed taxonomy — plain { world, kind:"fact", subject, predicate, object }
|
|
130
175
|
* objects, the exact shape src/domain/worlds-pack.mjs's isWorldFactRow
|
|
131
176
|
* reads. Deliberately NOT spider-1/fly-1: the board is reusable static
|
|
132
177
|
* content, minted game entities are a fresh session's own state
|
|
133
|
-
* (src/services/spider-fly.mjs's startSpiderFlyGame). */
|
|
178
|
+
* (src/services/spider-fly-turn.mjs's startSpiderFlyGame). */
|
|
134
179
|
export function* worldFactRows() {
|
|
135
180
|
for (let y = 1; y <= GRID_SIZE; y += 1) {
|
|
136
181
|
for (let x = 1; x <= GRID_SIZE; x += 1) {
|
|
@@ -245,10 +290,10 @@ export function isLiveRenderableAgent(id, state) {
|
|
|
245
290
|
|
|
246
291
|
/** A minimal, inert rule-row family, so scripts/build-worlds-pack.mjs's
|
|
247
292
|
* shared validator ("every world needs at least one rule row") passes.
|
|
248
|
-
* src/services/spider-fly.mjs never reads these back: grid movement is
|
|
249
|
-
* hand-written pathfinding over findActionPath/findReachableSet
|
|
250
|
-
*
|
|
251
|
-
*
|
|
293
|
+
* src/services/spider-fly-turn.mjs never reads these back: grid movement is
|
|
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. */
|
|
252
297
|
export function* worldRuleRows() {
|
|
253
298
|
yield {
|
|
254
299
|
world: WORLD_NAME, kind: "rule", name: "go", ruleKind: "action-signature",
|