AMD fact-sheet reader
Coerce a friendly Label: value fenced block into a dict — a companion parser
for AMD documents.
Overview
AMD documents are #/## headings with --- fences (the same shape quests,
clans, and universes are authored in). document_get_amd_file already parses the
headings and fences; this module is the companion data_parser that turns a
single fence's Label: value lines into a dict, with light value coercion:
| Helper | Coerces | Example |
|---|---|---|
amd_num |
int → float → str | 8 → 8 |
amd_pct |
percent or number | 40% → 0.4 |
amd_list |
comma list | a, b → ["a", "b"] |
amd_weighted |
name N weights |
by-the-book 40 → {"by_the_book": 40} |
amd_makeup |
N% name / list / scalar |
60% X, 40% Y → {"X": 60, "Y": 40} |
amd_coords |
first N ints | 6, 4 → [6, 4] |
amd_norm |
canonicalize a token | By-The-Book → by_the_book |
A fence that uses YAML flow ({ or [) is parsed as YAML instead, so YAML
fences keep working through the same reader.
The domain interpretation — what "Yields" or "Values" means — is the
caller's job, supplied via a handler(data, label, value) callback; this module
stays content-agnostic (just parsing + coercion). See the Open Universe's
universe_amd.py for a worked example.
Quick example
from sbs_utils.procedural.amd import amd_parse_facts, amd_list, amd_num
def handler(data, label, value):
if label == "yields":
data["yields"] = amd_list(value)
return True # consumed
return False # fall through to the default coercion
# fence_text is one `---` block: "Yields: ore 8\nReserve: 4000"
facts = amd_parse_facts(fence_text, handler=handler, default=amd_num)
# -> {"yields": ["ore 8"], "reserve": 4000}
handler receives the mutable data dict, so it can setdefault / nest /
append freely; return truthy to consume a label, falsy to let default(value)
handle it under amd_norm(label).
API
Friendly AMD fact-sheet fence reader.
A companion data_parser for procedural.quest.document_get_amd_file (which
already parses the #/## headings and --- fences): this turns a single
Label: value fenced block into a dict, with light value coercion - comma lists,
"name N" weights, "name N%" makeup, coord pairs, percentages. A block that uses
YAML flow ({ or [) is parsed as YAML instead, so YAML fences keep working
through the same reader.
Domain label->key interpretation (what "flies" or "yields" means) is the CALLER's
job, supplied via a handler callback - see the Open Universe's universe_amd.py
for a worked example. This module stays content-agnostic: just parsing + coercion.
Dependency-light (only load_yaml_string) so it imports cleanly and is
unit-testable outside the engine.
amd_coords(s, n=2)
'6, 4' -> [6, 4] (the first n signed-integer tokens).
amd_fact_lines(text)
Yield (label, value) per Label: value line - label lowercased, both
stripped. Skips blanks, // comments, and lines without a colon.
amd_is_yaml_flow(text)
True when the fence should be parsed as YAML (contains '{' or '[').
amd_list(s)
Comma-split, trimmed, empties dropped.
amd_makeup(s)
'60% X, 40% Y' -> {X:60, Y:40}; 'X, Y' -> list; 'X' -> str. (Three shapes; the percent form keeps the original display casing of the key.)
amd_norm(name)
Canonicalize a token: lowercase, hyphens/spaces -> underscores.
amd_num(s)
int -> float -> the trimmed string, whichever parses first.
amd_parse_facts(text, handler=None, default=amd_num)
Parse a friendly fact-sheet fence into a dict.
If amd_is_yaml_flow(text), delegate to load_yaml_string. Otherwise, for
each (label, value): call handler(data, label, value) when given, and if it
returns a truthy value the label is consumed; otherwise fall back to
data[amd_norm(label)] = default(value). The handler receives the mutable
data dict so it can setdefault / nest / append freely. Returns data.
amd_pct(s)
'40%' -> 0.4; '0.4' -> 0.4; a bare number -> float; else the string.
amd_weighted(s)
'by-the-book 40, fearsome 30' -> {by_the_book: 40, fearsome: 30} (trailing integer is the weight; a bare name gets weight 0).