@polycode-projects/the-mechanical-code-talker 2.7.23 → 2.7.25
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/data/sprites/adventurer.toml +5 -0
- package/data/sprites/animal.toml +6 -0
- package/data/sprites/butler.toml +8 -0
- package/data/sprites/cabinet.toml +5 -0
- package/data/sprites/container.toml +5 -0
- package/data/sprites/cook.toml +7 -0
- package/data/sprites/desk.toml +4 -0
- package/data/sprites/dog-with-colour.toml +20 -0
- package/data/sprites/dog.toml +5 -0
- package/data/sprites/egg.toml +4 -0
- package/data/sprites/fly.toml +5 -0
- package/data/sprites/furniture.toml +5 -0
- package/data/sprites/gardener.toml +4 -0
- package/data/sprites/housekeeper.toml +6 -0
- package/data/sprites/key.toml +5 -0
- package/data/sprites/lamp.toml +4 -0
- package/data/sprites/letter.toml +5 -0
- package/data/sprites/person.toml +4 -0
- package/data/sprites/poodle.toml +5 -0
- package/data/sprites/portable.toml +6 -0
- package/data/sprites/portrait.toml +5 -0
- package/data/sprites/room.toml +5 -0
- package/data/sprites/spider.toml +5 -0
- package/package.json +1 -1
- package/src/adapters/corpus/sprite-template-files.mjs +52 -0
- 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/domain/sprite-map.mjs +25 -10
- package/src/domain/sprite-templates.mjs +131 -0
- package/src/services/adventure-viz.mjs +201 -18
- 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 +73 -8
- package/src/services/spider-fly.mjs +56 -36
- package/src/surfaces/web/adventure-browser-entry.mjs +57 -19
- package/src/surfaces/web/memory-ask-browser.bundle.js +42 -0
- package/src/surfaces/web/spider-fly-browser-entry.mjs +2 -1
package/README.md
CHANGED
|
@@ -847,6 +847,29 @@ enabled = false # local-only counters; never phones home
|
|
|
847
847
|
[memory]
|
|
848
848
|
retention_versions = 5 # snapshot generations memory/core.mjs keeps on manifest bootstrap
|
|
849
849
|
backend = "sqlite" # default | memory | sqlite (see "Memory backends" above)
|
|
850
|
+
|
|
851
|
+
# Game tuning knobs (src/domain/game-config.mjs). Every OTHER game parameter
|
|
852
|
+
# (disk/peg counts, the goal, ...) lives in the game's own taught-English
|
|
853
|
+
# world instead — these are the handful of genuine magic numbers that
|
|
854
|
+
# aren't expressible that way.
|
|
855
|
+
[games.spider-fly]
|
|
856
|
+
spider_initial_mass = 15
|
|
857
|
+
spider_mass_decrement_per_turn = 0.5 # lower = slower to starve
|
|
858
|
+
fly_initial_mass = 10
|
|
859
|
+
fly_mass_decrement_per_turn = 1
|
|
860
|
+
vision_radius = 4 # Chebyshev radius an agent can see other agents within
|
|
861
|
+
egg_hatch_delay_turns = 3 # turns between a lay and its hatch
|
|
862
|
+
fly_spawn_interval_turns = 3 # a new fly arrives every Nth turn
|
|
863
|
+
eggs_eaten_threshold = 2 # flies eaten since the last egg before the next one lays
|
|
864
|
+
web_duration_turns = 10 # turns a spider-built web stays active
|
|
865
|
+
|
|
866
|
+
[games.guess-number]
|
|
867
|
+
default_lo = 1 # the range's default lower bound, when the opening line states none
|
|
868
|
+
default_hi = 100 # the range's default upper bound, when the opening line states none
|
|
869
|
+
max_bound = 1000000000 # sanity cap on either bound, however the opening line states it
|
|
870
|
+
|
|
871
|
+
[planning]
|
|
872
|
+
max_depth = 300 # the "solve it" plan lane's search-depth cap (hanoi, river-crossing, any taught-rule domain)
|
|
850
873
|
```
|
|
851
874
|
|
|
852
875
|
### Try it on an example graph
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# adventurer — the player's own sprite: the person silhouette plus a
|
|
2
|
+
# strap-and-satchel, so the one individual the player controls always
|
|
3
|
+
# reads as visually distinct from the room's own cast of NPCs.
|
|
4
|
+
classes = ["adventurer"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.6" r="3.3" fill="currentColor"/><path d="M6 21 C6 15.3 8.7 12.8 12 12.8 C15.3 12.8 18 15.3 18 21 Z" fill="currentColor"/><path d="M8.4 13.4 L17 17.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.85"/><rect x="14.6" y="15.8" width="5" height="4.6" rx="1" fill="currentColor" opacity="0.6"/><rect x="16.3" y="16.6" width="1.6" height="1.2" rx="0.3" fill="currentColor" opacity="0.9"/></svg>"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# animal — the deliberately plainest shape in the set: an unadorned
|
|
2
|
+
# four-legged blob, so an unregistered species reads as "some animal"
|
|
3
|
+
# rather than suggesting one the taxonomy never named. The declared root
|
|
4
|
+
# fallback for the spider-and-fly world.
|
|
5
|
+
classes = ["animal"]
|
|
6
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="13" rx="7" ry="4.4" fill="currentColor"/><circle cx="12" cy="7" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="7" y1="17" x2="7" y2="21"/><line x1="17" y1="17" x2="17" y2="21"/></g></svg>"""
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# butler — Ashcombe Hall's own butler: the person silhouette plus a stiff
|
|
2
|
+
# wing-collar flaring OUT past the shoulder line (changing the outer
|
|
3
|
+
# silhouette, not an internal opacity knockout — an internal light-on-dark
|
|
4
|
+
# accent was tried first and proved invisible once shrunk to 44px; a
|
|
5
|
+
# silhouette change survives shrinking the way gardener.toml's wide brim
|
|
6
|
+
# does) and a bow-tie.
|
|
7
|
+
classes = ["butler"]
|
|
8
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.6" r="3.3" fill="currentColor"/><path d="M6 21 C6 15.3 8.7 12.8 12 12.8 C15.3 12.8 18 15.3 18 21 Z" fill="currentColor"/><path d="M7.6 11.6 L11.3 13.2 L8.6 15.4 Z" fill="currentColor"/><path d="M16.4 11.6 L12.7 13.2 L15.4 15.4 Z" fill="currentColor"/><path d="M10.6 14.2 L12 16.1 L13.4 14.2 L12 18.6 Z" fill="currentColor" opacity="0.95"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# cabinet — Ashcombe Hall's own locked cabinet (a container): a two-door
|
|
2
|
+
# wardrobe silhouette, the gap between the doors reading as the seam, with
|
|
3
|
+
# two knobs and a cornice band.
|
|
4
|
+
classes = ["cabinet"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4.5" y="2.5" width="7.2" height="19" rx="1" fill="currentColor"/><rect x="12.3" y="2.5" width="7.2" height="19" rx="1" fill="currentColor"/><rect x="4.5" y="2.5" width="15" height="1.4" fill="currentColor" opacity="0.4"/><circle cx="10.6" cy="12" r="0.9" fill="currentColor" opacity="0.35"/><circle cx="13.4" cy="12" r="0.9" fill="currentColor" opacity="0.35"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# container — the generic fallback for any container with no sprite of
|
|
2
|
+
# its own (see cabinet/portrait for Ashcombe Hall's own two): a solid
|
|
3
|
+
# chest silhouette with a carved lid-seam and a latch highlight.
|
|
4
|
+
classes = ["container"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 10 L5 5 H19 L21 10 Z" fill="currentColor"/><rect x="3" y="10" width="18" height="10" rx="1.2" fill="currentColor"/><rect x="3" y="10" width="18" height="1.6" fill="currentColor" opacity="0.35"/><rect x="10.6" y="9.2" width="2.8" height="2.6" rx="0.5" fill="currentColor" opacity="0.85"/></svg>"""
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# cook — Ashcombe Hall's own cook: the person silhouette topped with a
|
|
2
|
+
# chef's toque puffing out WIDER than the head (the same "wider than the
|
|
3
|
+
# head reads as a separate garment at 44px" move gardener.toml's brim and
|
|
4
|
+
# housekeeper.toml's cap use — a same-width tall hat read as just an
|
|
5
|
+
# elongated head once shrunk, tried and rejected first).
|
|
6
|
+
classes = ["cook"]
|
|
7
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="3.4" rx="5.4" ry="3" fill="currentColor"/><rect x="8.6" y="5.6" width="6.8" height="2.2" rx="0.6" fill="currentColor"/><circle cx="12" cy="10" r="3.1" fill="currentColor"/><path d="M6 21 C6 15.6 8.7 13.2 12 13.2 C15.3 13.2 18 15.6 18 21 Z" fill="currentColor"/></svg>"""
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# desk — Ashcombe Hall's own study desk: a tabletop bar over two drawer
|
|
2
|
+
# blocks with pull-knobs, on tapered legs.
|
|
3
|
+
classes = ["desk"]
|
|
4
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="7" width="18" height="2.6" rx="0.6" fill="currentColor"/><rect x="4.4" y="9.6" width="6.6" height="5.6" rx="0.6" fill="currentColor" opacity="0.85"/><rect x="13" y="9.6" width="6.6" height="5.6" rx="0.6" fill="currentColor" opacity="0.85"/><circle cx="9.6" cy="12.4" r="0.55" fill="currentColor" opacity="0.4"/><circle cx="16.4" cy="12.4" r="0.55" fill="currentColor" opacity="0.4"/><path d="M4.6 15.2 L3.8 20.5 L5.6 20.5 L6.2 15.2 Z" fill="currentColor"/><path d="M17.8 15.2 L17.2 20.5 L19 20.5 L18.2 15.2 Z" fill="currentColor"/></svg>"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# dog-with-colour — the parameterized-property worked example
|
|
2
|
+
# (PLAN target: "a dog that is black"). Same silhouette as dog.toml, but
|
|
3
|
+
# every filled shape takes its color from the instance's OWN observed
|
|
4
|
+
# mgx:hasProperty value rather than the page's currentColor accent, with a
|
|
5
|
+
# thin currentColor stroke kept on each shape so a light value (white)
|
|
6
|
+
# still reads against a light card background. A property value with no
|
|
7
|
+
# entry in [parameters.colour.values] is not a match for this template at
|
|
8
|
+
# all (see sprite-templates.mjs's own header for the naming/specificity
|
|
9
|
+
# convention) — it falls through to the plain dog.toml sprite, never a
|
|
10
|
+
# guessed color.
|
|
11
|
+
classes = ["dog"]
|
|
12
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="13" cy="15" rx="6.4" ry="4.3" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/><circle cx="7" cy="9" r="3.6" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/><path d="M4.5 6.5 L2 3 L6 5 Z" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/><path d="M19 13 C22 12 22.5 15 20 16 Z" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="9" y1="19" x2="9" y2="22"/><line x1="17" y1="19" x2="17" y2="22"/></g></svg>"""
|
|
13
|
+
|
|
14
|
+
[parameters.colour]
|
|
15
|
+
property = "mgx:hasProperty"
|
|
16
|
+
placeholder = "{{FILL}}"
|
|
17
|
+
|
|
18
|
+
[parameters.colour.values]
|
|
19
|
+
black = "#22201d"
|
|
20
|
+
white = "#f5f2ea"
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# dog — the plain-class sprite a taxonomy hop away from poodle/sheepdog.
|
|
2
|
+
# Same body language as poodle's, without the pompom cut, so the family
|
|
3
|
+
# reads as one dog rather than two unrelated animals.
|
|
4
|
+
classes = ["dog"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="13" cy="15" rx="6.4" ry="4.3" fill="currentColor"/><circle cx="7" cy="9" r="3.6" fill="currentColor"/><ellipse cx="6.1" cy="8" rx="1" ry="1.3" fill="currentColor" opacity="0.3"/><path d="M4.5 6.5 L2 3 L6 5 Z" fill="currentColor"/><path d="M19 13 C22 12 22.5 15 20 16 Z" fill="currentColor"/><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="9" y1="19" x2="9" y2="22"/><line x1="17" y1="19" x2="17" y2="22"/></g></svg>"""
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# egg — the spider-and-fly board's laid egg. The plain shell silhouette
|
|
2
|
+
# plus one small carved highlight.
|
|
3
|
+
classes = ["egg"]
|
|
4
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2 C7 2 4 10 4 15.5 C4 20 7.6 22 12 22 C16.4 22 20 20 20 15.5 C20 10 17 2 12 2 Z" fill="currentColor"/><ellipse cx="9.6" cy="9.5" rx="1.7" ry="2.4" fill="currentColor" opacity="0.28"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# fly — the spider-and-fly board's other agent. A round head, a tapered
|
|
2
|
+
# body with a carved highlight, translucent wings, and two thin trailing
|
|
3
|
+
# legs for a touch of character at rest.
|
|
4
|
+
classes = ["fly"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="13.6" rx="3.4" ry="5.2" fill="currentColor"/><circle cx="12" cy="7" r="2.7" fill="currentColor"/><ellipse cx="11.1" cy="12" rx="1.1" ry="1.7" fill="currentColor" opacity="0.3"/><g fill="currentColor" opacity="0.55"><path d="M9 10 C3 6 2 4 3 2 C6 2 9 6 10.5 10 Z"/><path d="M15 10 C21 6 22 4 21 2 C18 2 15 6 13.5 10 Z"/></g><g stroke="currentColor" stroke-width="1" stroke-linecap="round"><path d="M9.6 17 L7.4 19.4"/><path d="M14.4 17 L16.6 19.4"/></g></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# furniture — the generic fallback for any furniture piece with no sprite
|
|
2
|
+
# of its own: a solid console-table silhouette (tapered legs, a carved
|
|
3
|
+
# highlight along the top edge), replacing the old hollow-shelf outline.
|
|
4
|
+
classes = ["furniture"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="6.5" width="17" height="3.2" rx="0.8" fill="currentColor"/><rect x="3.5" y="6.5" width="17" height="1" fill="currentColor" opacity="0.35"/><path d="M5.6 9.7 L4.6 20 L6.7 20 L7.2 9.7 Z" fill="currentColor"/><path d="M18.4 9.7 L16.8 20 L18.9 20 L19.4 9.7 Z" fill="currentColor"/></svg>"""
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# gardener — Ashcombe Hall's own gardener: the person silhouette under a
|
|
2
|
+
# wide sunhat brim.
|
|
3
|
+
classes = ["gardener"]
|
|
4
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="5.6" rx="7.4" ry="2" fill="currentColor"/><path d="M8.8 5.2 C8.8 3.1 10.2 1.6 12 1.6 C13.8 1.6 15.2 3.1 15.2 5.2 C15.2 5.6 15.1 6 15 6.3 L9 6.3 C8.9 6 8.8 5.6 8.8 5.2 Z" fill="currentColor" opacity="0.85"/><circle cx="12" cy="8.6" r="3.1" fill="currentColor"/><path d="M6 21 C6 15.5 8.7 13.1 12 13.1 C15.3 13.1 18 15.5 18 21 Z" fill="currentColor"/></svg>"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# housekeeper — Ashcombe Hall's own housekeeper: the person silhouette
|
|
2
|
+
# under a wide mob-cap brim (deliberately WIDER than the head, the same
|
|
3
|
+
# "wide silhouette reads at 44px" move gardener.toml's sunhat uses) and a
|
|
4
|
+
# tied apron line.
|
|
5
|
+
classes = ["housekeeper"]
|
|
6
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="4.6" rx="5.6" ry="2.1" fill="currentColor"/><path d="M8.6 5 C8.6 3 10 1.7 12 1.7 C14 1.7 15.4 3 15.4 5 L15.4 5.6 L8.6 5.6 Z" fill="currentColor" opacity="0.9"/><circle cx="12" cy="7.6" r="3.1" fill="currentColor"/><path d="M6 21 C6 15.5 8.7 13.1 12 13.1 C15.3 13.1 18 15.5 18 21 Z" fill="currentColor"/><path d="M9 16 C10 15.4 14 15.4 15 16" fill="none" stroke="currentColor" stroke-width="1" opacity="0.45" stroke-linecap="round"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# key — Ashcombe Hall's own key (hidden in the portrait, unlocks the
|
|
2
|
+
# cabinet): a classic round-bow skeleton key, bold strokes so it stays
|
|
3
|
+
# legible at a 44px render.
|
|
4
|
+
classes = ["key"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="7" cy="9" r="4.4" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M10.4 12 L19.5 21.1" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M16.4 17.1 L18.6 14.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M18.6 19.3 L20.6 17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>"""
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# lamp — Ashcombe Hall's own study lamp: a Victorian hurricane-lamp
|
|
2
|
+
# silhouette — base, glass globe, a neck, and a small carved flame.
|
|
3
|
+
classes = ["lamp"]
|
|
4
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21 H15 L14 18 H10 Z" fill="currentColor"/><path d="M8.4 18 C7 15.6 7 12.6 8.6 10.4 H15.4 C17 12.6 17 15.6 15.6 18 Z" fill="currentColor"/><rect x="10.6" y="6.4" width="2.8" height="4.4" fill="currentColor" opacity="0.85"/><path d="M12 3.2 C13.4 4.6 13.8 6 12 7.4 C10.2 6 10.6 4.6 12 3.2 Z" fill="currentColor" opacity="0.55"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# letter — Ashcombe Hall's own objective (hidden in the cabinet): an
|
|
2
|
+
# envelope with a folded flap (a lighter overlay, the same carved-highlight
|
|
3
|
+
# trick as the lid on container.toml) and a small wax seal.
|
|
4
|
+
classes = ["letter"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="1.2" fill="currentColor"/><path d="M3 6 L21 6 L21 6.8 L12 13.8 L3 6.8 Z" fill="currentColor" opacity="0.32"/><circle cx="12" cy="12.4" r="1.15" fill="currentColor" opacity="0.8"/></svg>"""
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# person — the generic fallback for anyone with no NPC sprite of their own
|
|
2
|
+
# (see butler/housekeeper/cook/gardener for the named Ashcombe Hall staff).
|
|
3
|
+
classes = ["person"]
|
|
4
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.6" r="3.3" fill="currentColor"/><path d="M6 21 C6 15.3 8.7 12.8 12 12.8 C15.3 12.8 18 15.3 18 21 Z" fill="currentColor"/><path d="M12 12.8 C13.1 12.8 14.1 13 15 13.4" fill="none" stroke="currentColor" stroke-width="0.9" opacity="0.4" stroke-linecap="round"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# poodle — the seed taxonomy's worked example (poodle IsA dog): its own
|
|
2
|
+
# pompom-cut head and tail, distinct from the plain dog sprite it would
|
|
3
|
+
# otherwise fall back to.
|
|
4
|
+
classes = ["poodle"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="13" cy="15" rx="6.4" ry="4.6" fill="currentColor"/><ellipse cx="10.8" cy="13.2" rx="1.8" ry="1.3" fill="currentColor" opacity="0.28"/><circle cx="7" cy="8.5" r="4" fill="currentColor"/><g fill="currentColor"><circle cx="4" cy="6" r="1.6"/><circle cx="7" cy="4.2" r="1.7"/><circle cx="10" cy="6" r="1.6"/></g><rect x="18.5" y="14" width="2.2" height="6" rx="1.1" fill="currentColor"/><circle cx="19.6" cy="13" r="1.5" fill="currentColor"/><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="9" y1="19.5" x2="9" y2="22"/><line x1="17" y1="19.5" x2="17" y2="22"/></g></svg>"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# portable — the generic fallback for any carryable object with no sprite
|
|
2
|
+
# of its own: a drawstring parcel silhouette, replacing the old
|
|
3
|
+
# generic-gift-box outline so it reads as "an unspecified small thing"
|
|
4
|
+
# rather than a gift.
|
|
5
|
+
classes = ["portable"]
|
|
6
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6 C7.5 6 5 9.4 5 13.4 C5 17.7 8.1 20 12 20 C15.9 20 19 17.7 19 13.4 C19 9.4 16.5 6 12 6 Z" fill="currentColor"/><path d="M9.4 6.4 C9.9 4.6 10.8 3.4 12 3.4 C13.2 3.4 14.1 4.6 14.6 6.4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><ellipse cx="9.8" cy="11.6" rx="1.6" ry="2.1" fill="currentColor" opacity="0.28"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# portrait — Ashcombe Hall's own drawing-room portrait (a container, hiding
|
|
2
|
+
# the key): a gilt arched frame around a lighter canvas, with a bust
|
|
3
|
+
# silhouette so it reads as a picture of someone, not an empty frame.
|
|
4
|
+
classes = ["portrait"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 21 V7 C4 4 6.8 2 12 2 C17.2 2 20 4 20 7 V21 Z" fill="currentColor"/><path d="M6.3 19.4 V8.3 C6.3 6.2 8.4 4.8 12 4.8 C15.6 4.8 17.7 6.2 17.7 8.3 V19.4 Z" fill="currentColor" opacity="0.28"/><circle cx="12" cy="10.4" r="2.3" fill="currentColor" opacity="0.7"/><path d="M8 18.4 C8 15.2 9.6 13.4 12 13.4 C14.4 13.4 16 15.2 16 18.4 Z" fill="currentColor" opacity="0.7"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# room — a schematic floor plan: a washed interior (a solid tint, not a
|
|
2
|
+
# hollow box) plus a doorway arch, kept abstract since a room is a space
|
|
3
|
+
# to draw, not an object to render literally.
|
|
4
|
+
classes = ["room"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/><rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M9 21 L9 13 A3 3 0 0 1 15 13 L15 21" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>"""
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# spider — the spider-and-fly board's own agent. A confident two-lobe body
|
|
2
|
+
# (a lighter carved patch on the abdomen for shading) and eight thin
|
|
3
|
+
# outward legs.
|
|
4
|
+
classes = ["spider"]
|
|
5
|
+
svg = """<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="16" rx="5.2" ry="4.8" fill="currentColor"/><ellipse cx="10.3" cy="14.3" rx="1.8" ry="1.4" fill="currentColor" opacity="0.3"/><circle cx="12" cy="9.6" r="3.2" fill="currentColor"/><g stroke="currentColor" stroke-width="1.1" fill="none" stroke-linecap="round"><path d="M9.4 8 L2.2 4.4"/><path d="M9 10.2 L1.4 9.4"/><path d="M9.2 12.6 L2 14.2"/><path d="M10 15.4 L3.4 19"/><path d="M14.6 8 L21.8 4.4"/><path d="M15 10.2 L22.6 9.4"/><path d="M14.8 12.6 L22 14.2"/><path d="M14 15.4 L20.6 19"/></g></svg>"""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.25",
|
|
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; no codebase index of its own.",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// sprite-template-files.mjs — the filesystem side of the sprite template
|
|
2
|
+
// library: read data/sprites/*.toml and hand the raw parsed tables to
|
|
3
|
+
// src/domain/sprite-templates.mjs's pure resolver, mirroring
|
|
4
|
+
// construction-banks.mjs's own readConstructionFiles idiom exactly (same
|
|
5
|
+
// directory-of-toml-files shape, same per-file-defensive load).
|
|
6
|
+
//
|
|
7
|
+
// This module is Node-only (readdirSync/readFileSync) and is never imported
|
|
8
|
+
// by src/surfaces/web/*-browser-entry.mjs — the browser pages get their
|
|
9
|
+
// template set as embedded page JSON instead (adventure-viz.mjs's/
|
|
10
|
+
// spider-fly-viz.mjs's own renderAdventureHtml/renderSpiderFlyHtml already
|
|
11
|
+
// embed the world/grid payload the same way; scripts/build-demo-site.mjs
|
|
12
|
+
// calls this loader once, in Node, and hands the result to those render
|
|
13
|
+
// functions), the same reason Ashcombe Hall's own JSONL world source is read
|
|
14
|
+
// once at build time rather than bundled into the browser.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { parse as parseToml } from "smol-toml";
|
|
20
|
+
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
/** The sprite-template directory (data, not code) — every *.toml file inside
|
|
23
|
+
* is loaded, in filename order, so a new sprite is a new committed file,
|
|
24
|
+
* never an edit to this loader. */
|
|
25
|
+
export const SPRITE_TEMPLATES_DIR = join(HERE, "..", "..", "..", "data", "sprites");
|
|
26
|
+
|
|
27
|
+
/** Read every *.toml file in `dir` (sorted, deterministic) and return the raw
|
|
28
|
+
* parsed template objects as a flat array. A missing directory or an
|
|
29
|
+
* unparseable file is DEFENSIVE (per-file: a broken file is skipped, not
|
|
30
|
+
* fatal to the others, the same posture readConstructionFiles takes for its
|
|
31
|
+
* own bank) — a production page load can never go blank because one hand-
|
|
32
|
+
* authored sprite has a typo. `test/adapters/sprite-templates.test.mjs`
|
|
33
|
+
* parses the real files directly (not through this lenient loader) so a
|
|
34
|
+
* broken file still fails a test loudly. */
|
|
35
|
+
export function readSpriteTemplateFiles(dir = SPRITE_TEMPLATES_DIR) {
|
|
36
|
+
let files;
|
|
37
|
+
try {
|
|
38
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".toml")).sort();
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const templates = [];
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = parseToml(readFileSync(join(dir, file), "utf8"));
|
|
46
|
+
templates.push(parsed);
|
|
47
|
+
} catch {
|
|
48
|
+
continue; // one malformed file never takes the others down
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return templates;
|
|
52
|
+
}
|
|
@@ -117,6 +117,14 @@ export async function normalizeConfig(raw, { configDir } = {}) {
|
|
|
117
117
|
if (src.extensions !== undefined) cfg.extensions = src.extensions;
|
|
118
118
|
if (src.bias !== undefined) cfg.bias = src.bias;
|
|
119
119
|
|
|
120
|
+
// Game tuning knobs (src/domain/game-config.mjs): sparse PASS-THROUGH only,
|
|
121
|
+
// same discipline as [extensions]/[bias] above — the raw `[games.*]`/
|
|
122
|
+
// `[planning]` tables ride through unmodified (snake_case keys); mapping
|
|
123
|
+
// them onto the internal camelCase shape and filling in unset keys from
|
|
124
|
+
// the shipped defaults is resolveGameConfig's job, never this module's.
|
|
125
|
+
if (src.games !== undefined) cfg.games = src.games;
|
|
126
|
+
if (src.planning !== undefined) cfg.planning = src.planning;
|
|
127
|
+
|
|
120
128
|
const idx = src.index || {};
|
|
121
129
|
const index = {};
|
|
122
130
|
if (idx.languages !== undefined) index.languages = idx.languages;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// game-config.mjs — the shipped defaults for every game's tuning knobs
|
|
2
|
+
// (spider-fly's mass economy, guess-the-number's default/max bounds, the
|
|
3
|
+
// shared plan lane's search-depth cap) and the pure function that folds a
|
|
4
|
+
// normalized tmct.toml's [games]/[planning] tables over them.
|
|
5
|
+
//
|
|
6
|
+
// Every other game parameter lives in the game's own taught-English world
|
|
7
|
+
// definition (data/games/hanoi-3.txt and friends) and never needs a knob
|
|
8
|
+
// here — only a genuine magic number with no taught-fact home does.
|
|
9
|
+
//
|
|
10
|
+
// Pure: no filesystem access, no module-level mutable state. resolveGameConfig
|
|
11
|
+
// is safe to call once per session and the result handed around freely —
|
|
12
|
+
// several games/tests running in the same process never share or mutate one
|
|
13
|
+
// another's resolved config.
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_GAME_CONFIG = Object.freeze({
|
|
16
|
+
spiderFly: Object.freeze({
|
|
17
|
+
spiderInitialMass: 15,
|
|
18
|
+
spiderMassDecrementPerTurn: 0.5,
|
|
19
|
+
flyInitialMass: 10,
|
|
20
|
+
flyMassDecrementPerTurn: 1,
|
|
21
|
+
visionRadius: 4,
|
|
22
|
+
eggHatchDelayTurns: 3,
|
|
23
|
+
flySpawnIntervalTurns: 3,
|
|
24
|
+
eggsEatenThreshold: 2,
|
|
25
|
+
webDurationTurns: 10,
|
|
26
|
+
}),
|
|
27
|
+
guessNumber: Object.freeze({
|
|
28
|
+
defaultLo: 1,
|
|
29
|
+
defaultHi: 100,
|
|
30
|
+
maxBound: 1_000_000_000,
|
|
31
|
+
}),
|
|
32
|
+
planning: Object.freeze({
|
|
33
|
+
maxDepth: 300,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// snake_case tmct.toml key -> camelCase internal key, one map per table —
|
|
38
|
+
// mirrors how toml-config.mjs's normalizeConfig maps every other section
|
|
39
|
+
// (e.g. [tune]) onto its own internal shape.
|
|
40
|
+
const SPIDER_FLY_KEY_MAP = Object.freeze({
|
|
41
|
+
spider_initial_mass: "spiderInitialMass",
|
|
42
|
+
spider_mass_decrement_per_turn: "spiderMassDecrementPerTurn",
|
|
43
|
+
fly_initial_mass: "flyInitialMass",
|
|
44
|
+
fly_mass_decrement_per_turn: "flyMassDecrementPerTurn",
|
|
45
|
+
vision_radius: "visionRadius",
|
|
46
|
+
egg_hatch_delay_turns: "eggHatchDelayTurns",
|
|
47
|
+
fly_spawn_interval_turns: "flySpawnIntervalTurns",
|
|
48
|
+
eggs_eaten_threshold: "eggsEatenThreshold",
|
|
49
|
+
web_duration_turns: "webDurationTurns",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const GUESS_NUMBER_KEY_MAP = Object.freeze({
|
|
53
|
+
default_lo: "defaultLo",
|
|
54
|
+
default_hi: "defaultHi",
|
|
55
|
+
max_bound: "maxBound",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const PLANNING_KEY_MAP = Object.freeze({
|
|
59
|
+
max_depth: "maxDepth",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** `defaults` with every key `keyMap` names overridden by its raw snake_case
|
|
63
|
+
* counterpart in `raw`, when actually present — every unset sibling keeps
|
|
64
|
+
* the default, so the result is always fully populated. */
|
|
65
|
+
function mergeSection(defaults, raw, keyMap) {
|
|
66
|
+
const out = { ...defaults };
|
|
67
|
+
if (!raw || typeof raw !== "object") return out;
|
|
68
|
+
for (const [tomlKey, camelKey] of Object.entries(keyMap)) {
|
|
69
|
+
if (raw[tomlKey] !== undefined) out[camelKey] = raw[tomlKey];
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fold a normalized tmct.toml's `games`/`planning` tables (the raw sparse
|
|
76
|
+
* pass-through src/adapters/toml-config.mjs's normalizeConfig produces —
|
|
77
|
+
* snake_case keys, present only when actually set in the file) over
|
|
78
|
+
* DEFAULT_GAME_CONFIG. `toml` may be null/undefined (no tmct.toml, or one
|
|
79
|
+
* that failed to load) — every key then falls back to its default. Returns a
|
|
80
|
+
* fully populated object of the same shape as DEFAULT_GAME_CONFIG; toml value
|
|
81
|
+
* wins per key when present, default otherwise.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveGameConfig(toml) {
|
|
84
|
+
const games = toml?.games ?? {};
|
|
85
|
+
return {
|
|
86
|
+
spiderFly: mergeSection(DEFAULT_GAME_CONFIG.spiderFly, games["spider-fly"], SPIDER_FLY_KEY_MAP),
|
|
87
|
+
guessNumber: mergeSection(DEFAULT_GAME_CONFIG.guessNumber, games["guess-number"], GUESS_NUMBER_KEY_MAP),
|
|
88
|
+
planning: mergeSection(DEFAULT_GAME_CONFIG.planning, toml?.planning, PLANNING_KEY_MAP),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -25,9 +25,12 @@ export const WEB_DURATION_TURNS = 10;
|
|
|
25
25
|
// FLY_INITIAL_MASS/FLY_MASS_DECREMENT_PER_TURN): a spider starves like a fly
|
|
26
26
|
// does, and gains exactly a fly's remaining mass on an eat. Heavier starting
|
|
27
27
|
// mass than a single fly's worth on purpose — a spider that eats nothing for
|
|
28
|
-
// a while has some runway before starving.
|
|
28
|
+
// a while has some runway before starving. The decrement is half a fly's own
|
|
29
|
+
// (spiders live longer between meals than flies do), and — like every other
|
|
30
|
+
// tunable here — overridable per session via tmct.toml's [games.spider-fly]
|
|
31
|
+
// (src/domain/game-config.mjs).
|
|
29
32
|
export const SPIDER_INITIAL_MASS = 15;
|
|
30
|
-
export const SPIDER_MASS_DECREMENT_PER_TURN =
|
|
33
|
+
export const SPIDER_MASS_DECREMENT_PER_TURN = 0.5;
|
|
31
34
|
|
|
32
35
|
export const cellId = (x, y) => `cell-${x}-${y}`;
|
|
33
36
|
|
|
@@ -154,26 +154,41 @@ function directSuperclassesOf(term, factRows) {
|
|
|
154
154
|
return out;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
export function resolveSpriteForClass(className, factRows, spriteRegistry, { rootFallback = "animal" } = {}) {
|
|
157
|
+
/** The full rdfs:subClassOf ancestor chain for `className`, nearest-first,
|
|
158
|
+
* INCLUDING the starting term itself as `chain[0]` — the same breadth-first
|
|
159
|
+
* walk resolveSpriteForClass performs below, extracted so
|
|
160
|
+
* sprite-templates.mjs's richer, property-aware resolver can repeat the
|
|
161
|
+
* same specificity check at each level (fully-specific variant >
|
|
162
|
+
* parameterized template > plain class sprite) without re-deriving the BFS.
|
|
163
|
+
* Pure; `factRows` is read only, never mutated. */
|
|
164
|
+
export function classAncestorChain(className, factRows) {
|
|
166
165
|
const start = normFactTerm(className);
|
|
167
166
|
const seen = new Set();
|
|
168
167
|
const queue = [start];
|
|
168
|
+
const chain = [];
|
|
169
169
|
while (queue.length) {
|
|
170
170
|
const term = queue.shift();
|
|
171
171
|
if (seen.has(term)) continue;
|
|
172
172
|
seen.add(term);
|
|
173
|
-
|
|
173
|
+
chain.push(term);
|
|
174
174
|
for (const parent of directSuperclassesOf(term, factRows)) {
|
|
175
175
|
if (!seen.has(parent)) queue.push(parent);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
+
return chain;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve a class name to a sprite: `className` itself if the registry
|
|
183
|
+
* carries it directly, otherwise the nearest rdfs:subClassOf ancestor
|
|
184
|
+
* (breadth-first, nearest first — styled after ask.mjs's own ancestorsOf)
|
|
185
|
+
* that the registry carries, otherwise `rootFallback` ("animal" by default —
|
|
186
|
+
* override it for a registry whose own declared root is "object" or "plant").
|
|
187
|
+
* Pure; `factRows` is read only, never mutated.
|
|
188
|
+
*/
|
|
189
|
+
export function resolveSpriteForClass(className, factRows, spriteRegistry, { rootFallback = "animal" } = {}) {
|
|
190
|
+
for (const term of classAncestorChain(className, factRows)) {
|
|
191
|
+
if (Object.prototype.hasOwnProperty.call(spriteRegistry, term)) return spriteRegistry[term];
|
|
192
|
+
}
|
|
178
193
|
return spriteRegistry[rootFallback];
|
|
179
194
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// sprite-templates.mjs — the property-aware sprite resolver, layered ON TOP
|
|
2
|
+
// of sprite-map.mjs's flat class registry rather than replacing it (its own
|
|
3
|
+
// header explains why the ancestor walk stays exactly as-is). Where
|
|
4
|
+
// resolveSpriteForClass only ever asks "what class is this", this module also
|
|
5
|
+
// asks "what does this INSTANCE'S OWN mgx:hasProperty-shaped fact say about
|
|
6
|
+
// it" — the operator's own worked example: a dog typed "dog" with a taught
|
|
7
|
+
// "black" property resolves to a filled-in coloured template, one typed
|
|
8
|
+
// "sheepdog" with the same property resolves through the ordinary ancestor
|
|
9
|
+
// walk to the SAME template, and a dog with no colour property at all falls
|
|
10
|
+
// through to the plain dog sprite.
|
|
11
|
+
//
|
|
12
|
+
// Naming convention for the two template kinds a specific class can carry
|
|
13
|
+
// beyond its own plain `{class}.toml` (data/sprites/*.toml, one file per
|
|
14
|
+
// sprite):
|
|
15
|
+
// - a PARAMETERIZED template — `{class}-with-{property}.toml` (this pass
|
|
16
|
+
// ships `dog-with-colour.toml`) — declares a `[parameters.<name>]` table
|
|
17
|
+
// naming the fact predicate that carries the value (`property`, e.g.
|
|
18
|
+
// "mgx:hasProperty"), a placeholder token the `svg` string contains
|
|
19
|
+
// (`placeholder`, e.g. "{{FILL}}"), and a `[parameters.<name>.values]`
|
|
20
|
+
// table translating an observed property VALUE into the literal
|
|
21
|
+
// substitution (e.g. `black = "#22201d"`). A value with no entry in that
|
|
22
|
+
// map is not a match for this template at all — it falls through to a
|
|
23
|
+
// less specific one, never a guessed/invented substitution.
|
|
24
|
+
// - a fully-specific hand-authored VARIANT — `{class}-with-{property}-
|
|
25
|
+
// {value}.toml` (e.g. a hypothetical `dog-with-colour-black.toml`, not
|
|
26
|
+
// authored this pass) — carries the same `classes` as the class it
|
|
27
|
+
// specializes, plus a `[match]` table (`property`, `value`) naming the
|
|
28
|
+
// exact fact it requires, so it outranks the parameterized template
|
|
29
|
+
// when both would otherwise apply.
|
|
30
|
+
//
|
|
31
|
+
// Specificity order, checked at EACH term of the class's ancestor chain
|
|
32
|
+
// (nearest first, sprite-map.mjs's own classAncestorChain) before moving to
|
|
33
|
+
// the next ancestor: an exact fully-specific variant whose [match] is
|
|
34
|
+
// satisfied > a parameterized template filled with an observed matching
|
|
35
|
+
// value > a plain class template > (repeat at the next ancestor) > the
|
|
36
|
+
// existing flat spriteRegistry entry for that same term (so a class not yet
|
|
37
|
+
// migrated to its own template keeps resolving exactly as it did before this
|
|
38
|
+
// module existed) > once the chain is exhausted, the same three-step check
|
|
39
|
+
// against `rootFallback`, falling back to spriteRegistry's own root entry
|
|
40
|
+
// only if nothing there matches either.
|
|
41
|
+
import { classAncestorChain } from "./sprite-map.mjs";
|
|
42
|
+
|
|
43
|
+
/** Every template in `templates` whose `classes` list names `term`. */
|
|
44
|
+
function templatesForClass(term, templates) {
|
|
45
|
+
return (templates || []).filter((t) => Array.isArray(t?.classes) && t.classes.includes(term));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function matchSatisfied(match, propertyFacts) {
|
|
49
|
+
if (!match || !match.property || match.value === undefined) return false;
|
|
50
|
+
return (propertyFacts || []).some((f) => f.predicate === match.property && f.object === match.value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Fill a parameterized template's `svg` from the first of its own
|
|
54
|
+
* `[parameters.*]` whose observed property value maps to a substitution —
|
|
55
|
+
* or null when no property fact names a mapped value (never a guess). */
|
|
56
|
+
function parameterizedFill(template, propertyFacts) {
|
|
57
|
+
for (const param of Object.values(template.parameters || {})) {
|
|
58
|
+
const values = param?.values || {};
|
|
59
|
+
const hit = (propertyFacts || []).find(
|
|
60
|
+
(f) => f.predicate === param.property && Object.prototype.hasOwnProperty.call(values, f.object),
|
|
61
|
+
);
|
|
62
|
+
if (hit) return template.svg.split(param.placeholder).join(values[hit.object]);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Resolve ONE class term (no ancestor walk here — the caller repeats this
|
|
68
|
+
* at every level of the chain) against the template set, in specificity
|
|
69
|
+
* order: fully-specific match variant > parameterized template filled with
|
|
70
|
+
* an observed value > plain class template. Returns the SVG string, or null
|
|
71
|
+
* when nothing at this level matches. */
|
|
72
|
+
function resolveAtTerm(term, propertyFacts, templates) {
|
|
73
|
+
const candidates = templatesForClass(term, templates);
|
|
74
|
+
const matched = candidates.find((t) => t.match && matchSatisfied(t.match, propertyFacts));
|
|
75
|
+
if (matched) return matched.svg;
|
|
76
|
+
for (const t of candidates) {
|
|
77
|
+
if (t.match || !t.parameters) continue;
|
|
78
|
+
const filled = parameterizedFill(t, propertyFacts);
|
|
79
|
+
if (filled) return filled;
|
|
80
|
+
}
|
|
81
|
+
const plain = candidates.find((t) => !t.match && !t.parameters);
|
|
82
|
+
return plain ? plain.svg : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve `className` to sprite SVG markup, property-aware: the ancestor
|
|
87
|
+
* chain (from `factRows`, sprite-map.mjs's own walk) is checked nearest-
|
|
88
|
+
* first, and at each term the template set (`templates`, the parsed
|
|
89
|
+
* data/sprites/*.toml set) is tried before falling back to `spriteRegistry`
|
|
90
|
+
* for that same term — so a class with no template at all resolves exactly
|
|
91
|
+
* as resolveSpriteForClass already does. `propertyFacts` is the instance's
|
|
92
|
+
* own small `{predicate, object}` fact set (e.g. its mgx:hasProperty rows) —
|
|
93
|
+
* read only, never required to be non-empty. Pure.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveSpriteAsset(className, factRows, propertyFacts, templates, spriteRegistry, { rootFallback = "animal" } = {}) {
|
|
96
|
+
for (const term of classAncestorChain(className, factRows)) {
|
|
97
|
+
const hit = resolveAtTerm(term, propertyFacts, templates);
|
|
98
|
+
if (hit) return hit;
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(spriteRegistry, term)) return spriteRegistry[term];
|
|
100
|
+
}
|
|
101
|
+
const rootHit = resolveAtTerm(rootFallback, propertyFacts, templates);
|
|
102
|
+
if (rootHit) return rootHit;
|
|
103
|
+
return spriteRegistry[rootFallback];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every internal-consistency problem with one parsed template, as plain
|
|
107
|
+
* strings — empty when the template is well-formed. Used both by
|
|
108
|
+
* test/adapters/sprite-templates.test.mjs (against the real loaded
|
|
109
|
+
* data/sprites/ directory) and available to any future loader that wants to
|
|
110
|
+
* warn rather than silently drop a broken file. Checks: `classes` is a
|
|
111
|
+
* non-empty array, `svg` is a real `<svg` string, a `[parameters.*]` table
|
|
112
|
+
* names a `property` and a non-empty `values` map, and a `[match]` table
|
|
113
|
+
* names both `property` and `value`. */
|
|
114
|
+
export function spriteTemplateProblems(template) {
|
|
115
|
+
const problems = [];
|
|
116
|
+
const t = template || {};
|
|
117
|
+
if (!Array.isArray(t.classes) || t.classes.length === 0) problems.push("classes is missing or empty");
|
|
118
|
+
if (typeof t.svg !== "string" || !t.svg.trim().startsWith("<svg")) problems.push("svg is missing or not an <svg> string");
|
|
119
|
+
for (const [name, param] of Object.entries(t.parameters || {})) {
|
|
120
|
+
if (!param?.property) problems.push(`parameters.${name}.property is missing`);
|
|
121
|
+
if (!param?.placeholder) problems.push(`parameters.${name}.placeholder is missing`);
|
|
122
|
+
else if (typeof t.svg === "string" && !t.svg.includes(param.placeholder)) {
|
|
123
|
+
problems.push(`parameters.${name}.placeholder ${JSON.stringify(param.placeholder)} does not appear in svg`);
|
|
124
|
+
}
|
|
125
|
+
if (!param?.values || Object.keys(param.values).length === 0) problems.push(`parameters.${name}.values is empty`);
|
|
126
|
+
}
|
|
127
|
+
if (t.match && (!t.match.property || t.match.value === undefined)) {
|
|
128
|
+
problems.push("match is missing property or value");
|
|
129
|
+
}
|
|
130
|
+
return problems;
|
|
131
|
+
}
|