From f52cb4cca99f8a493580dff78a407e1484dfa3c1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 20:50:45 -0800 Subject: [PATCH] Story wire: read a step's structured args Arduino-Source steps now carry their action's arguments as an `args` object (flags, whole numbers, words) next to a plain `variant` name, instead of "mart CeruleanCity_Mart budget=1500" text. The schema and the Python reader accept it with the same value types as the C++ codec, and the shared wire cases cover it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- data/story/wire-conformance.json | 44 ++++++++++++++++++++++++++++ schemas/story-checkpoint.schema.json | 1 + src/pokesim/story/checkpoint_wire.py | 21 ++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/data/story/wire-conformance.json b/data/story/wire-conformance.json index 5f151b7..8a46122 100644 --- a/data/story/wire-conformance.json +++ b/data/story/wire-conformance.json @@ -255,6 +255,50 @@ "value": {"map": "M", "certainty": "unknown", "where": {"kind": "gps"}} }, "expectation": {"rejected": true} + }, + { + "input": { + "name": "args", + "record": "step", + "value": { + "id": "buy", + "kind": "game_action", + "variant": "mart", + "args": {"map": "CeruleanCity_Mart", "budget": 3000, "if_present": true} + } + }, + "expectation": { + "canonical": { + "id": "buy", + "kind": "game_action", + "variant": "mart", + "args": {"map": "CeruleanCity_Mart", "budget": 3000, "if_present": true} + } + } + }, + { + "input": { + "name": "fractional arg", + "record": "step", + "value": {"id": "a", "kind": "game_action", "variant": "mart", "args": {"budget": 1.5}} + }, + "expectation": {"rejected": true} + }, + { + "input": { + "name": "nested arg", + "record": "step", + "value": {"id": "a", "kind": "game_action", "variant": "mart", "args": {"items": [1]}} + }, + "expectation": {"rejected": true} + }, + { + "input": { + "name": "args not an object", + "record": "step", + "value": {"id": "a", "kind": "game_action", "variant": "mart", "args": ["map"]} + }, + "expectation": {"rejected": true} } ] } diff --git a/schemas/story-checkpoint.schema.json b/schemas/story-checkpoint.schema.json index c4deee3..8b27a9b 100644 --- a/schemas/story-checkpoint.schema.json +++ b/schemas/story-checkpoint.schema.json @@ -120,6 +120,7 @@ } }, "variant": {"type": "string"}, + "args": {"type": "object", "additionalProperties": {"type": ["boolean", "integer", "string"]}}, "skip_if": {"type": "array", "items": {"type": "string"}}, "once_per_save": {"type": "boolean"}, "notes": {"$ref": "#/$defs/notes"} diff --git a/src/pokesim/story/checkpoint_wire.py b/src/pokesim/story/checkpoint_wire.py index a9647f0..dac782d 100644 --- a/src/pokesim/story/checkpoint_wire.py +++ b/src/pokesim/story/checkpoint_wire.py @@ -49,6 +49,22 @@ def array(value): return value +def read_args(value): + """A step's args: flags, whole numbers and words by name, as the step's + action declares them in the game's C++ (StoryArgSpec); this reader checks + only the value types.""" + if not isinstance(value, dict): + raise ValueError('expected args object') + result = {} + for key, item in value.items(): + if type(item) is int: + integer(item, -9223372036854775808, 9223372036854775807) + elif type(item) is not bool and not isinstance(item, str): + raise ValueError('arg is not a flag, whole number or word: ' + key) + result[key] = item + return result + + def read_notes(root, result): """Copy non-empty `notes` paragraphs; no executor reads them.""" notes = [text(v) for v in array(root.get('notes', []))] @@ -79,7 +95,7 @@ def read_pose(value): def read_step(value): root = fields(value, ('id', 'kind', 'why', 'target', 'direction', 'legs', 'fight_wild', - 'yes', 'may_lose', 'battles', 'items', 'variant', 'skip_if', 'declared_after', + 'yes', 'may_lose', 'battles', 'items', 'variant', 'args', 'skip_if', 'declared_after', 'once_per_save', 'notes')) result = dict(id=text(root['id']), kind=choice(root['kind'], KINDS)) if not result['id']: @@ -115,6 +131,9 @@ def read_step(value): items.append(dict(item=text(item['item']), quantity=integer(item['quantity'], 0, 9223372036854775807))) if items: result['items'] = items + args = read_args(root.get('args', {})) + if args: + result['args'] = args skip = [text(v) for v in array(root.get('skip_if', []))] if skip: result['skip_if'] = skip