@polycode-projects/the-mechanical-code-talker 2.7.23 → 2.7.24
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/README.md +23 -0
- package/package.json +1 -1
- package/src/adapters/toml-config.mjs +8 -0
- package/src/domain/game-config.mjs +90 -0
- package/src/domain/spider-fly-world.mjs +5 -2
- package/src/services/adventure-viz.mjs +140 -13
- package/src/services/adventure.mjs +45 -3
- package/src/services/chat-session.mjs +9 -1
- package/src/services/chat.mjs +49 -27
- package/src/services/spider-fly-turn.mjs +11 -10
- package/src/services/spider-fly-viz.mjs +53 -0
- package/src/services/spider-fly.mjs +56 -36
- package/src/surfaces/web/adventure-browser-entry.mjs +56 -19
- package/src/surfaces/web/memory-ask-browser.bundle.js +42 -0
package/src/services/chat.mjs
CHANGED
|
@@ -54,6 +54,7 @@ import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
|
54
54
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
55
55
|
import { adventureTurn, unclaimedAdventureOpening } from "./adventure.mjs";
|
|
56
56
|
import { spiderFlyTurn } from "./spider-fly-turn.mjs";
|
|
57
|
+
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
57
58
|
|
|
58
59
|
// Composition: the chat surface supplies the domain parser's default lemma/POS
|
|
59
60
|
// adapter (the browser bundle's ask-nlp stub carries no factory, so this is a
|
|
@@ -1559,6 +1560,16 @@ export function renderVerbose(last) {
|
|
|
1559
1560
|
function conversationalTurn(line, ctx) {
|
|
1560
1561
|
const raw = String(line);
|
|
1561
1562
|
const q = raw.toLowerCase().replace(/[.!?]+$/, "").replace(/\s+/g, " ").trim();
|
|
1563
|
+
// A live game (adventure/spider-fly/guess-the-number) already recognizes its
|
|
1564
|
+
// OWN exact stop phrase ("stop playing", "I give up", ...) before this lane
|
|
1565
|
+
// ever sees the line, so a bare word reaching here mid-game was never meant
|
|
1566
|
+
// as a farewell — it's an in-game noun that fell through every game-shaped
|
|
1567
|
+
// command check (e.g. "player", the adventure's own subject). The fuzzy-typo
|
|
1568
|
+
// fallback below is a GUESS (bounded edit distance against "later" etc.),
|
|
1569
|
+
// and a wrong guess ends the whole session — too costly a mistake to risk
|
|
1570
|
+
// mid-game. The exact/closed-set farewell just above and below this guard
|
|
1571
|
+
// stays live either way (a real "bye"/"exit" is unambiguous, never a guess).
|
|
1572
|
+
const gameActive = Boolean(ctx.planHolder?.state?.adventure || ctx.planHolder?.state?.spiderFly || ctx.planHolder?.state?.game);
|
|
1562
1573
|
const t = (id, slots = {}) => tRender(ctx.templates, id, slots) ?? TEMPLATES_UNAVAILABLE;
|
|
1563
1574
|
const mk = (answer, { end = false, miss = false, via = "template", lane = null } = {}) => {
|
|
1564
1575
|
const ts = new Date().toISOString();
|
|
@@ -1686,8 +1697,10 @@ function conversationalTurn(line, ctx) {
|
|
|
1686
1697
|
// Fuzzy-typo fallback (A4): every exact/collapsed closed-set lookup above missed —
|
|
1687
1698
|
// try a bounded edit-distance match against the flattened conversational phrase
|
|
1688
1699
|
// pool ("helo", "thnx", "wat r u", "byee"), restricted to short non-code-ish
|
|
1689
|
-
// input so a genuine near-miss structural question is never grabbed.
|
|
1690
|
-
|
|
1700
|
+
// input so a genuine near-miss structural question is never grabbed. Skipped
|
|
1701
|
+
// entirely mid-game (see gameActive above) — never reached the CLI's own
|
|
1702
|
+
// process-exit path from a guess before this fix existed.
|
|
1703
|
+
if (!gameActive) {
|
|
1691
1704
|
const fuzzyHit = fuzzyConversationalMatch(raw);
|
|
1692
1705
|
if (fuzzyHit) {
|
|
1693
1706
|
const bucket = classifyConversational(fuzzyHit);
|
|
@@ -10054,7 +10067,7 @@ const sameGoalSpec = (a, b) =>
|
|
|
10054
10067
|
* taught action rules (PLAN_HANOI's chat surface). Returns
|
|
10055
10068
|
* { text, via, deduced, note, plan? } or null when the query is none of the
|
|
10056
10069
|
* three shapes. Mutates planHolder.state (the session's plan slot). */
|
|
10057
|
-
async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", }) {
|
|
10070
|
+
async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
10058
10071
|
let q = String(query).trim();
|
|
10059
10072
|
// GOAL REVISION — "actually the goal is …", "instead, the goal is …", "the
|
|
10060
10073
|
// goal is now …": a revision marker ahead of (or inside) a goal frame means
|
|
@@ -10349,9 +10362,10 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
10349
10362
|
return { text: `I can't compile that goal: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence (uncompilable goal)", note: "plan lane — goal compile decline" };
|
|
10350
10363
|
}
|
|
10351
10364
|
const { findActionPath } = await import("../domain/planning.mjs");
|
|
10365
|
+
const maxDepth = gameConfig?.planning?.maxDepth ?? DEFAULT_GAME_CONFIG.planning.maxDepth;
|
|
10352
10366
|
let found;
|
|
10353
10367
|
try {
|
|
10354
|
-
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth
|
|
10368
|
+
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth, stateKey: stateKeyFor });
|
|
10355
10369
|
} catch (err) {
|
|
10356
10370
|
if (err instanceof PlanBudgetError) {
|
|
10357
10371
|
return { text: `the search space is too large (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "plan a move sequence (budget exceeded)", note: "plan lane — budget decline" };
|
|
@@ -10360,7 +10374,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
10360
10374
|
}
|
|
10361
10375
|
if (!found) {
|
|
10362
10376
|
return {
|
|
10363
|
-
text: `no plan found within
|
|
10377
|
+
text: `no plan found within ${maxDepth} moves from the current state to: ${goalText}.`,
|
|
10364
10378
|
via: "plan", deduced: "plan a move sequence (no path)", note: "plan lane — honest miss: findActionPath returned null",
|
|
10365
10379
|
};
|
|
10366
10380
|
}
|
|
@@ -10635,7 +10649,7 @@ const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s
|
|
|
10635
10649
|
* than silently accepted alongside the current location. */
|
|
10636
10650
|
const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
|
|
10637
10651
|
|
|
10638
|
-
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null }) {
|
|
10652
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
10639
10653
|
const ts = new Date().toISOString();
|
|
10640
10654
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
10641
10655
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
@@ -11006,7 +11020,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11006
11020
|
// orientation card before this lane ever ran.
|
|
11007
11021
|
let planResult = null;
|
|
11008
11022
|
if (!handled && miss && memoryDir && planHolder) {
|
|
11009
|
-
const planLane = await planLaneAnswer(query, { memoryDir, planHolder, sessionId });
|
|
11023
|
+
const planLane = await planLaneAnswer(query, { memoryDir, planHolder, sessionId, gameConfig });
|
|
11010
11024
|
if (planLane) {
|
|
11011
11025
|
answer = planLane.text; via = planLane.via; recordMiss = false; handled = true;
|
|
11012
11026
|
if (planLane.lane) dialogueLaneOverride = planLane.lane;
|
|
@@ -12449,22 +12463,23 @@ async function skosRelatedAnswer(memoryDir, query, cache) {
|
|
|
12449
12463
|
* captures (\S+) so a non-numeric bound is SEEN and declined rather than
|
|
12450
12464
|
* silently defaulted. */
|
|
12451
12465
|
const GAME_BOUNDS_CLAUSE_RE = /\b(?:between\s+(\S+)\s+and\s+(\S+)|up\s+to\s+(\S+))\b/i;
|
|
12452
|
-
const GAME_BOUND_MAX = 1_000_000_000;
|
|
12453
12466
|
|
|
12454
|
-
/** The bounds an opening line states — { lo, hi } (default
|
|
12455
|
-
* { problem } naming why the stated range is
|
|
12456
|
-
|
|
12467
|
+
/** The bounds an opening line states — { lo, hi } (default `gameConfig`'s own
|
|
12468
|
+
* defaultLo/defaultHi), or { problem } naming why the stated range is
|
|
12469
|
+
* unplayable. `gameConfig` defaults to DEFAULT_GAME_CONFIG.guessNumber. */
|
|
12470
|
+
function parseGameBounds(text, gameConfig = DEFAULT_GAME_CONFIG.guessNumber) {
|
|
12471
|
+
const { defaultLo, defaultHi, maxBound } = gameConfig;
|
|
12457
12472
|
const m = String(text).match(GAME_BOUNDS_CLAUSE_RE);
|
|
12458
|
-
if (!m) return { lo:
|
|
12459
|
-
const tokens = (m[3] !== undefined ? [
|
|
12473
|
+
if (!m) return { lo: defaultLo, hi: defaultHi };
|
|
12474
|
+
const tokens = (m[3] !== undefined ? [String(defaultLo), m[3]] : [m[1], m[2]])
|
|
12460
12475
|
.map((t) => String(t).replace(/[,.?!]+$/, ""));
|
|
12461
12476
|
if (!tokens.every((t) => /^-?\d+$/.test(t))) {
|
|
12462
|
-
return { problem:
|
|
12477
|
+
return { problem: `I can only play with whole-number bounds — say "between ${defaultLo} and ${defaultHi}".` };
|
|
12463
12478
|
}
|
|
12464
12479
|
const lo = Number(tokens[0]);
|
|
12465
12480
|
const hi = Number(tokens[1]);
|
|
12466
|
-
if (Math.abs(lo) >
|
|
12467
|
-
return { problem: `that range is too big for a fair game — keep both bounds within ${
|
|
12481
|
+
if (Math.abs(lo) > maxBound || Math.abs(hi) > maxBound) {
|
|
12482
|
+
return { problem: `that range is too big for a fair game — keep both bounds within ${maxBound.toLocaleString("en-US")}.` };
|
|
12468
12483
|
}
|
|
12469
12484
|
if (hi < lo) return { problem: `no number is between ${lo} and ${hi} — that range is empty. Put the smaller bound first.` };
|
|
12470
12485
|
if (hi === lo) return { problem: `between ${lo} and ${hi} leaves exactly one number, so there is nothing to guess. Pick a wider range.` };
|
|
@@ -12485,20 +12500,21 @@ const THINKER_OPEN_TAIL_RE = /^[\s,.!?—-]*(?:and\s+)?(?:i\s*(?:'ll|\s+will)\s+
|
|
|
12485
12500
|
const INVITATION_OPEN_LEAD_RE = /^(?:let'?s\s+play|wanna\s+play|want\s+to\s+play|can\s+we\s+play|shall\s+we\s+play|do\s+you\s+want\s+to\s+play|will\s+you\s+play|play)\s+(?:a\s+)?(?:game\s+of\s+)?(?:guess[- ]the[- ]number|number[- ]guessing(?:\s+game)?|guessing\s+game)\b(.*)$/i;
|
|
12486
12501
|
const INVITATION_OPEN_TAIL_RE = /^[\s,.!?—-]*(?:with\s+me|together)?[\s,.!?—-]*$/i;
|
|
12487
12502
|
|
|
12488
|
-
/** An opening move — { mode, bounds } — or null.
|
|
12489
|
-
|
|
12503
|
+
/** An opening move — { mode, bounds } — or null. `gameConfig` defaults to
|
|
12504
|
+
* DEFAULT_GAME_CONFIG.guessNumber and threads through to parseGameBounds. */
|
|
12505
|
+
function matchGameOpening(line, gameConfig = DEFAULT_GAME_CONFIG.guessNumber) {
|
|
12490
12506
|
const l = String(line).trim();
|
|
12491
12507
|
const guesser = l.match(GUESSER_OPEN_LEAD_RE);
|
|
12492
12508
|
if (guesser && GUESSER_OPEN_TAIL_RE.test(guesser[1].replace(GAME_BOUNDS_CLAUSE_RE, " "))) {
|
|
12493
|
-
return { mode: "guesser", bounds: parseGameBounds(l) };
|
|
12509
|
+
return { mode: "guesser", bounds: parseGameBounds(l, gameConfig) };
|
|
12494
12510
|
}
|
|
12495
12511
|
const thinker = l.match(THINKER_OPEN_LEAD_RE);
|
|
12496
12512
|
if (thinker && THINKER_OPEN_TAIL_RE.test(thinker[1].replace(GAME_BOUNDS_CLAUSE_RE, " "))) {
|
|
12497
|
-
return { mode: "thinker", bounds: parseGameBounds(l) };
|
|
12513
|
+
return { mode: "thinker", bounds: parseGameBounds(l, gameConfig) };
|
|
12498
12514
|
}
|
|
12499
12515
|
const invite = l.match(INVITATION_OPEN_LEAD_RE);
|
|
12500
12516
|
if (invite && INVITATION_OPEN_TAIL_RE.test(invite[1].replace(GAME_BOUNDS_CLAUSE_RE, " "))) {
|
|
12501
|
-
return { mode: "thinker", bounds: parseGameBounds(l) };
|
|
12517
|
+
return { mode: "thinker", bounds: parseGameBounds(l, gameConfig) };
|
|
12502
12518
|
}
|
|
12503
12519
|
return null;
|
|
12504
12520
|
}
|
|
@@ -12611,10 +12627,10 @@ function gameContinuationAnswer(line, game, planHolder) {
|
|
|
12611
12627
|
/** The whole game lane for one turn: continuations first (active game only),
|
|
12612
12628
|
* then opening moves, with the one-at-a-time declines both ways across the
|
|
12613
12629
|
* shared plan slot. Null when the turn is not the game's to answer. */
|
|
12614
|
-
function guessNumberTurn(line, { planHolder, env }) {
|
|
12630
|
+
function guessNumberTurn(line, { planHolder, env, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
12615
12631
|
const state = planHolder?.state ?? null;
|
|
12616
12632
|
const game = state?.game ?? null;
|
|
12617
|
-
const opening = matchGameOpening(line);
|
|
12633
|
+
const opening = matchGameOpening(line, gameConfig?.guessNumber);
|
|
12618
12634
|
if (game) {
|
|
12619
12635
|
const continuation = gameContinuationAnswer(line, game, planHolder);
|
|
12620
12636
|
if (continuation) return continuation;
|
|
@@ -12855,7 +12871,13 @@ function vocabAntecedentFrom(last) {
|
|
|
12855
12871
|
return m[1];
|
|
12856
12872
|
}
|
|
12857
12873
|
|
|
12858
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, _noSplit = false } = {}) {
|
|
12874
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, _noSplit = false } = {}) {
|
|
12875
|
+
// Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
|
|
12876
|
+
// bounds, the shared plan lane's search-depth cap) — a caller's own
|
|
12877
|
+
// gameConfig (chat-session.mjs resolves one per session from tmct.toml)
|
|
12878
|
+
// wins outright; direct/library callers that omit it get the shipped
|
|
12879
|
+
// defaults, byte-identical to before this seam existed.
|
|
12880
|
+
const resolvedGameConfig = gameConfig ?? DEFAULT_GAME_CONFIG;
|
|
12859
12881
|
const line = String(input ?? "").trim();
|
|
12860
12882
|
// ONE fresh, empty cache for this turn only — every factRows() reader
|
|
12861
12883
|
// reached from this call shares it, so the first reader computes
|
|
@@ -12912,7 +12934,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
12912
12934
|
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
12913
12935
|
// it untouched, and the caller re-threads whatever comes back.
|
|
12914
12936
|
const planHolder = { state: planState };
|
|
12915
|
-
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder };
|
|
12937
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig };
|
|
12916
12938
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
12917
12939
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
12918
12940
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -12973,7 +12995,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
12973
12995
|
// line would otherwise read as a declarative to remember. A mid-game line
|
|
12974
12996
|
// matching no game shape returns null here and the game stands untouched.
|
|
12975
12997
|
{
|
|
12976
|
-
const gameTurn = guessNumberTurn(workingLine, { planHolder, env });
|
|
12998
|
+
const gameTurn = guessNumberTurn(workingLine, { planHolder, env, gameConfig: resolvedGameConfig });
|
|
12977
12999
|
if (gameTurn) {
|
|
12978
13000
|
note(trace, `lane: ${gameTurn.note}`);
|
|
12979
13001
|
if (gameTurn.goal) note(trace, `goal: ${gameTurn.goal}`);
|
|
@@ -13015,7 +13037,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13015
13037
|
// opening line would otherwise read as a declarative or an orientation ask.
|
|
13016
13038
|
{
|
|
13017
13039
|
const sfTurn = await spiderFlyTurn(workingLine, {
|
|
13018
|
-
planHolder, memoryDir, env, cache: factRowsCache, isPlanFrameLine,
|
|
13040
|
+
planHolder, memoryDir, env, cache: factRowsCache, isPlanFrameLine, gameConfig: resolvedGameConfig,
|
|
13019
13041
|
});
|
|
13020
13042
|
if (sfTurn) {
|
|
13021
13043
|
note(trace, `lane: ${sfTurn.note}`);
|
|
@@ -22,6 +22,7 @@ import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame } from "./spid
|
|
|
22
22
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
23
23
|
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
24
24
|
import { appendFacts, appendRule, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
25
|
+
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
25
26
|
|
|
26
27
|
// ---- recognizers: the closed opening/stop/tick/address set -------------------
|
|
27
28
|
|
|
@@ -70,7 +71,7 @@ const WORLD_OPENING_FALLBACK =
|
|
|
70
71
|
|
|
71
72
|
// ---- the opening turn: load the shipped board through the worlds pack -------
|
|
72
73
|
|
|
73
|
-
async function openSpiderFlyGame({ planHolder, memoryDir, env, cache }) {
|
|
74
|
+
async function openSpiderFlyGame({ planHolder, memoryDir, env, cache, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
74
75
|
if (!memoryDir) {
|
|
75
76
|
return {
|
|
76
77
|
text: "the spider-and-fly game needs a session with a memory store to hold the board — start tmct inside a repo first.",
|
|
@@ -99,7 +100,7 @@ async function openSpiderFlyGame({ planHolder, memoryDir, env, cache }) {
|
|
|
99
100
|
}
|
|
100
101
|
if (cache) cache.rows = null; // the fact-rows cache predates these writes
|
|
101
102
|
|
|
102
|
-
const { started } = await startSpiderFlyGame(memoryDir, { flyCount: 1 });
|
|
103
|
+
const { started } = await startSpiderFlyGame(memoryDir, { flyCount: 1, config: gameConfig?.spiderFly });
|
|
103
104
|
planHolder.state = { spiderFly: { turn: 0 } };
|
|
104
105
|
const opener = started
|
|
105
106
|
? (payload.meta?.opening || WORLD_OPENING_FALLBACK)
|
|
@@ -212,8 +213,8 @@ function describeEcologyNote(eco) {
|
|
|
212
213
|
return bits.length ? `; ${bits.join(", ")}` : "";
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [], addressedNote = null }) {
|
|
216
|
-
const tick = await runSpiderFlyTick(memoryDir, { toldFacts });
|
|
216
|
+
async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [], addressedNote = null, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
217
|
+
const tick = await runSpiderFlyTick(memoryDir, { toldFacts, config: gameConfig?.spiderFly });
|
|
217
218
|
if (cache) cache.rows = null;
|
|
218
219
|
planHolder.state = { spiderFly: { turn: tick.turn } };
|
|
219
220
|
return {
|
|
@@ -232,7 +233,7 @@ async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [],
|
|
|
232
233
|
* current turn's told-facts"). This also matches how runSpiderFlyTick
|
|
233
234
|
* itself already works: it holds no standing plan or belief between calls,
|
|
234
235
|
* recomputing everything fresh from the folded fact rows every tick. */
|
|
235
|
-
async function runToldFactTurn(match, { planHolder, memoryDir, cache }) {
|
|
236
|
+
async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
236
237
|
const [, addrKindRaw, addrNum, subjKindRaw, subjNum, direction, cellLiteral] = match;
|
|
237
238
|
const addrKind = addrKindRaw.toLowerCase();
|
|
238
239
|
const subjKind = subjKindRaw.toLowerCase();
|
|
@@ -258,7 +259,7 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache }) {
|
|
|
258
259
|
const targetCellId = cellId(targetCell.x, targetCell.y);
|
|
259
260
|
const toldFacts = [{ subject: subjectId, toAgent: addresseeId, cell: targetCellId, turn: state.turnCount + 1 }];
|
|
260
261
|
return runTickAndRender({
|
|
261
|
-
planHolder, memoryDir, cache, toldFacts,
|
|
262
|
+
planHolder, memoryDir, cache, toldFacts, gameConfig,
|
|
262
263
|
addressedNote: `told the ${addresseeId} the ${subjectId} is at ${targetCellId}`,
|
|
263
264
|
});
|
|
264
265
|
}
|
|
@@ -275,7 +276,7 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache }) {
|
|
|
275
276
|
* ordinary lanes unchanged, board untouched (§6.2 — no special-cased
|
|
276
277
|
* spider-fly code path for plain questions).
|
|
277
278
|
*/
|
|
278
|
-
export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache = null, isPlanFrameLine = () => false }) {
|
|
279
|
+
export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache = null, isPlanFrameLine = () => false, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
279
280
|
const slot = planHolder?.state ?? null;
|
|
280
281
|
const spiderFly = slot?.spiderFly ?? null;
|
|
281
282
|
const opening = SPIDER_FLY_OPEN_RE.test(line);
|
|
@@ -305,7 +306,7 @@ export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache =
|
|
|
305
306
|
note: "SPIDER-FLY — an opening arrived while a plan frame is active; the slot holds one thing at a time",
|
|
306
307
|
};
|
|
307
308
|
}
|
|
308
|
-
return openSpiderFlyGame({ planHolder, memoryDir, env, cache });
|
|
309
|
+
return openSpiderFlyGame({ planHolder, memoryDir, env, cache, gameConfig });
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
// A game is live.
|
|
@@ -343,11 +344,11 @@ export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache =
|
|
|
343
344
|
miss: true,
|
|
344
345
|
};
|
|
345
346
|
}
|
|
346
|
-
return runToldFactTurn(told, { planHolder, memoryDir, cache });
|
|
347
|
+
return runToldFactTurn(told, { planHolder, memoryDir, cache, gameConfig });
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
if (SPIDER_FLY_TICK_RE.test(line)) {
|
|
350
|
-
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [] });
|
|
351
|
+
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [], gameConfig });
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
return null; // an unaddressed aside — the ordinary lanes answer, board untouched
|
|
@@ -171,6 +171,11 @@ ${THEME_TOKENS_CSS}
|
|
|
171
171
|
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
172
172
|
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
|
|
173
173
|
.chatask input:disabled { opacity: .5; }
|
|
174
|
+
.chatpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; }
|
|
175
|
+
.pill { font-family: ${MONO_STACK}; font-size: .68rem; padding: .2rem .6rem; border: 1px solid var(--line); border-radius: 99px; background: var(--bg); color: var(--ink); white-space: nowrap; }
|
|
176
|
+
.pill:hover:not(:disabled) { border-color: var(--taught); }
|
|
177
|
+
.pill:disabled { opacity: .45; cursor: default; }
|
|
178
|
+
.pill[data-role="addr"].active { border-color: var(--taught); color: var(--taught); }
|
|
174
179
|
.controls-row { display: flex; align-items: center; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
|
|
175
180
|
.controls-row button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .3rem .7rem; border: 1px solid var(--line); background: var(--card); color: var(--ink); }
|
|
176
181
|
.controls-row button:hover:not(:disabled) { border-color: var(--taught); }
|
|
@@ -206,6 +211,14 @@ ${THEME_TOKENS_CSS}
|
|
|
206
211
|
<span class="prompt mono">tmct></span>
|
|
207
212
|
<input id="chatq" type="text" placeholder="@spider the fly is east" aria-label="Address the spider or the fly" disabled>
|
|
208
213
|
</form>
|
|
214
|
+
<div class="chatpills" id="chatpills" role="group" aria-label="quick phrases to fill the chat input">
|
|
215
|
+
<button type="button" class="pill" data-role="addr" data-addressee="spider" disabled>@spider</button>
|
|
216
|
+
<button type="button" class="pill" data-role="addr" data-addressee="fly" disabled>@fly</button>
|
|
217
|
+
<button type="button" class="pill" data-role="dir" data-direction="north" disabled>the fly is north</button>
|
|
218
|
+
<button type="button" class="pill" data-role="dir" data-direction="south" disabled>the fly is south</button>
|
|
219
|
+
<button type="button" class="pill" data-role="dir" data-direction="east" disabled>the fly is east</button>
|
|
220
|
+
<button type="button" class="pill" data-role="dir" data-direction="west" disabled>the fly is west</button>
|
|
221
|
+
</div>
|
|
209
222
|
</div>
|
|
210
223
|
</aside>
|
|
211
224
|
</div>
|
|
@@ -238,6 +251,9 @@ const SPIDERFLY = ${gridData};
|
|
|
238
251
|
const chatlogEl = el("chatlog");
|
|
239
252
|
const chatformEl = el("chatform");
|
|
240
253
|
const chatqEl = el("chatq");
|
|
254
|
+
const chatpillsEl = el("chatpills");
|
|
255
|
+
const addressPillEls = [...chatpillsEl.querySelectorAll('[data-role="addr"]')];
|
|
256
|
+
const directionPillEls = [...chatpillsEl.querySelectorAll('[data-role="dir"]')];
|
|
241
257
|
const statusEl = el("status");
|
|
242
258
|
const turnLabelEl = el("turnLabel");
|
|
243
259
|
const resetBtn = el("resetBtn");
|
|
@@ -465,6 +481,42 @@ const SPIDERFLY = ${gridData};
|
|
|
465
481
|
});
|
|
466
482
|
});
|
|
467
483
|
|
|
484
|
+
// ---- chat pills: click-to-fill shortcuts over the SAME #chatq input, never
|
|
485
|
+
// a second path into the engine — a pill only ever sets/appends text and
|
|
486
|
+
// focuses the field, exactly what typing the same characters would do, so
|
|
487
|
+
// free typing keeps working unchanged and every resulting phrase is one the
|
|
488
|
+
// addressed teach-frame grammar (SPIDER_FLY_TOLD_RE in spider-fly-turn.mjs)
|
|
489
|
+
// genuinely accepts.
|
|
490
|
+
function addresseeKindOf(value) {
|
|
491
|
+
const m = /^@(spider|fly)(?:-\\d+)?\\b/i.exec(String(value).trim());
|
|
492
|
+
return m ? m[1].toLowerCase() : null;
|
|
493
|
+
}
|
|
494
|
+
function refreshPills() {
|
|
495
|
+
const explicitKind = addresseeKindOf(chatqEl.value);
|
|
496
|
+
const subject = (explicitKind || "spider") === "spider" ? "fly" : "spider";
|
|
497
|
+
for (const btn of directionPillEls) btn.textContent = "the " + subject + " is " + btn.dataset.direction;
|
|
498
|
+
for (const btn of addressPillEls) btn.classList.toggle("active", btn.dataset.addressee === explicitKind);
|
|
499
|
+
}
|
|
500
|
+
for (const btn of addressPillEls) {
|
|
501
|
+
btn.addEventListener("click", () => {
|
|
502
|
+
chatqEl.value = "@" + btn.dataset.addressee + " ";
|
|
503
|
+
refreshPills();
|
|
504
|
+
chatqEl.focus();
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
for (const btn of directionPillEls) {
|
|
508
|
+
btn.addEventListener("click", () => {
|
|
509
|
+
const kind = addresseeKindOf(chatqEl.value) || "spider";
|
|
510
|
+
let value = chatqEl.value;
|
|
511
|
+
if (!addresseeKindOf(value)) value = "@" + kind + " " + value.trimStart();
|
|
512
|
+
chatqEl.value = value.replace(/\\s+$/, "") + " " + btn.textContent;
|
|
513
|
+
refreshPills();
|
|
514
|
+
chatqEl.focus();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
chatqEl.addEventListener("input", refreshPills);
|
|
518
|
+
refreshPills();
|
|
519
|
+
|
|
468
520
|
// ---- serialize every engine-touching call: the ticker and the chat dock
|
|
469
521
|
// share one in-memory store, and an overlapping tick()/turn() pair could
|
|
470
522
|
// race against the same @turnN write.
|
|
@@ -481,6 +533,7 @@ const SPIDERFLY = ${gridData};
|
|
|
481
533
|
statusEl.textContent = session.opening;
|
|
482
534
|
chatqEl.disabled = false;
|
|
483
535
|
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false;
|
|
536
|
+
for (const btn of [...addressPillEls, ...directionPillEls]) btn.disabled = false;
|
|
484
537
|
}
|
|
485
538
|
|
|
486
539
|
let loopScheduled = false;
|