Merge pull request '[TRAINING] Add Evennia telemetry, sample trace, and baseline eval (#37)' (#40) from codex/evennia-training into main
This commit was merged in pull request #40.
This commit is contained in:
@@ -2,24 +2,52 @@ import json
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_BASE_DIR = "~/.timmy/training-data/evennia"
|
||||||
|
|
||||||
def telemetry_dir(base_dir: str | Path = "~/.timmy/training-data/evennia") -> Path:
|
|
||||||
|
def telemetry_dir(base_dir: str | Path = DEFAULT_BASE_DIR) -> Path:
|
||||||
return Path(base_dir).expanduser()
|
return Path(base_dir).expanduser()
|
||||||
|
|
||||||
|
|
||||||
def event_log_path(session_id: str, base_dir: str | Path = "~/.timmy/training-data/evennia") -> Path:
|
def _session_day() -> str:
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y%m%d")
|
||||||
|
|
||||||
|
|
||||||
|
def event_log_path(session_id: str, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path:
|
||||||
session_id = (session_id or "unbound").strip() or "unbound"
|
session_id = (session_id or "unbound").strip() or "unbound"
|
||||||
day = datetime.now(timezone.utc).strftime("%Y%m%d")
|
return telemetry_dir(base_dir) / _session_day() / f"{session_id}.jsonl"
|
||||||
return telemetry_dir(base_dir) / day / f"{session_id}.jsonl"
|
|
||||||
|
|
||||||
|
|
||||||
def append_event(session_id: str, event: dict, base_dir: str | Path = "~/.timmy/training-data/evennia") -> Path:
|
def session_meta_path(session_id: str, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path:
|
||||||
|
session_id = (session_id or "unbound").strip() or "unbound"
|
||||||
|
return telemetry_dir(base_dir) / _session_day() / f"{session_id}.meta.json"
|
||||||
|
|
||||||
|
|
||||||
|
def write_session_metadata(session_id: str, metadata: dict, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path:
|
||||||
|
path = session_meta_path(session_id, base_dir)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {}
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
payload = {}
|
||||||
|
payload.update(metadata or {})
|
||||||
|
payload.setdefault("session_id", session_id)
|
||||||
|
payload.setdefault("event_log_path", str(event_log_path(session_id, base_dir)))
|
||||||
|
payload.setdefault("updated_at", datetime.now(timezone.utc).isoformat())
|
||||||
|
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def append_event(session_id: str, event: dict, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path:
|
||||||
path = event_log_path(session_id, base_dir)
|
path = event_log_path(session_id, base_dir)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
payload = dict(event)
|
payload = dict(event)
|
||||||
payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
|
payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
|
||||||
with path.open("a", encoding="utf-8") as f:
|
with path.open("a", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
||||||
|
write_session_metadata(session_id, {"last_event_excerpt": excerpt(json.dumps(payload, ensure_ascii=False), 400)}, base_dir)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
19
evennia_tools/training.py
Normal file
19
evennia_tools/training.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
WORLD_BASICS_STEPS = (
|
||||||
|
{"command": "look", "expected": ("Gate",)},
|
||||||
|
{"command": "enter", "expected": ("Courtyard",)},
|
||||||
|
{"command": "workshop", "expected": ("Workshop", "Workbench")},
|
||||||
|
{"command": "look", "expected": ("Workshop", "Workbench")},
|
||||||
|
{"command": "courtyard", "expected": ("Courtyard", "Map Table")},
|
||||||
|
{"command": "chapel", "expected": ("Chapel", "Prayer Wall")},
|
||||||
|
{"command": "look Book of the Soul", "expected": ("Book of the Soul", "doctrinal anchor")},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def example_trace_path(repo_root: str | Path) -> Path:
|
||||||
|
return Path(repo_root) / "training-data" / "evennia" / "examples" / "world-basics-trace.example.jsonl"
|
||||||
|
|
||||||
|
|
||||||
|
def example_eval_path(repo_root: str | Path) -> Path:
|
||||||
|
return Path(repo_root) / "training-data" / "evennia" / "examples" / "world-basics-eval.example.json"
|
||||||
35
reports/production/2026-03-28-evennia-training-baseline.md
Normal file
35
reports/production/2026-03-28-evennia-training-baseline.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Evennia Training Proof — 2026-03-28
|
||||||
|
|
||||||
|
Issue:
|
||||||
|
- #37 Hermes/Evennia telemetry, replay, and DPO/eval alignment
|
||||||
|
|
||||||
|
What this slice adds:
|
||||||
|
- canonical telemetry contract for the Evennia lane
|
||||||
|
- session-id sidecar mapping path
|
||||||
|
- sample trace generator
|
||||||
|
- deterministic replay/eval harness for world basics
|
||||||
|
- committed example trace/eval artifacts
|
||||||
|
|
||||||
|
Committed example artifacts:
|
||||||
|
- `training-data/evennia/examples/world-basics-trace.example.jsonl`
|
||||||
|
- `training-data/evennia/examples/world-basics-eval.example.json`
|
||||||
|
|
||||||
|
Final result:
|
||||||
|
- replay/eval now starts from a deterministic Gate anchor using a dedicated eval account (`TimmyEval`)
|
||||||
|
- sample trace generation succeeds
|
||||||
|
- world-basics eval passes cleanly
|
||||||
|
- orientation: pass
|
||||||
|
- navigation: pass
|
||||||
|
- object inspection: pass
|
||||||
|
|
||||||
|
Canonical mapping:
|
||||||
|
- Hermes session id is the join key
|
||||||
|
- world events write to `~/.timmy/training-data/evennia/YYYYMMDD/<session_id>.jsonl`
|
||||||
|
- sidecar mapping file writes to `~/.timmy/training-data/evennia/YYYYMMDD/<session_id>.meta.json`
|
||||||
|
- the bridge binds the session id through `mcp_evennia_bind_session`
|
||||||
|
|
||||||
|
Why this matters:
|
||||||
|
- world interaction no longer disappears into an opaque side channel
|
||||||
|
- we now have a path from Hermes transcript -> Evennia event log -> replay/eval
|
||||||
|
- this complements rather than replaces NLE/MiniHack
|
||||||
|
- the persistent-world lane now has a real green baseline, not just an aspiration
|
||||||
1
scripts/evennia/__init__.py
Normal file
1
scripts/evennia/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Evennia world-lane scripts."""
|
||||||
78
scripts/evennia/eval_world_basics.py
Normal file
78
scripts/evennia/eval_world_basics.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(REPO_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
|
from evennia_tools.training import WORLD_BASICS_STEPS, example_eval_path
|
||||||
|
from scripts.evennia import evennia_mcp_server as bridge
|
||||||
|
|
||||||
|
EVENNIA_BIN = Path.home() / '.timmy' / 'evennia' / 'venv' / 'bin' / 'evennia'
|
||||||
|
GAME_DIR = Path.home() / '.timmy' / 'evennia' / 'timmy_world'
|
||||||
|
EVAL_USERNAME = os.environ.get("TIMMY_EVENNIA_EVAL_USERNAME", "TimmyEval")
|
||||||
|
EVAL_PASSWORD = os.environ.get("TIMMY_EVENNIA_EVAL_PASSWORD", "timmy-eval-world-dev")
|
||||||
|
|
||||||
|
|
||||||
|
def reset_timmy_to_gate():
|
||||||
|
env = dict(**os.environ)
|
||||||
|
env['PYTHONPATH'] = str(REPO_ROOT) + ':' + env.get('PYTHONPATH', '')
|
||||||
|
code = f"from evennia.accounts.models import AccountDB; from evennia.accounts.accounts import DefaultAccount; from evennia.utils.search import search_object; acc=AccountDB.objects.filter(username='{EVAL_USERNAME}').first(); acc = acc or DefaultAccount.create(username='{EVAL_USERNAME}', password='{EVAL_PASSWORD}')[0]; char=list(acc.characters)[0]; gate=search_object('Gate', exact=True)[0]; char.home=gate; char.move_to(gate, quiet=True, move_hooks=False); char.save(); print('RESET_OK')"
|
||||||
|
subprocess.run([str(EVENNIA_BIN), 'shell', '-c', code], cwd=GAME_DIR, env=env, check=True, capture_output=True, text=True, timeout=120)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_to_gate() -> None:
|
||||||
|
output = bridge._observe("timmy").get("output", "")
|
||||||
|
if not output:
|
||||||
|
output = bridge._command("look", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
for _ in range(6):
|
||||||
|
if "Gate" in output:
|
||||||
|
return
|
||||||
|
if "Courtyard" in output:
|
||||||
|
output = bridge._command("gate", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
continue
|
||||||
|
if any(room in output for room in ("Workshop", "Archive", "Chapel")):
|
||||||
|
output = bridge._command("courtyard", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
continue
|
||||||
|
output = bridge._command("look", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
bridge._disconnect("timmy")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
reset_timmy_to_gate()
|
||||||
|
bridge._save_bound_session_id(os.environ.get("TIMMY_EVENNIA_EVAL_SESSION_ID", "eval-evennia-world-basics"))
|
||||||
|
bridge._connect(name="timmy", username=EVAL_USERNAME, password=EVAL_PASSWORD)
|
||||||
|
normalize_to_gate()
|
||||||
|
results = []
|
||||||
|
for step in WORLD_BASICS_STEPS:
|
||||||
|
command = step["command"]
|
||||||
|
expected = step["expected"]
|
||||||
|
res = bridge._command(command, name="timmy", wait_ms=400)
|
||||||
|
output = res.get("output", "")
|
||||||
|
passed = all(token in output for token in expected)
|
||||||
|
results.append({"command": command, "expected": expected, "passed": passed, "output_excerpt": output[:300]})
|
||||||
|
bridge._disconnect("timmy")
|
||||||
|
summary = {
|
||||||
|
"passed": all(item["passed"] for item in results),
|
||||||
|
"checks": results,
|
||||||
|
"orientation": next((item["passed"] for item in results if item["command"] == "look"), False),
|
||||||
|
"navigation": all(item["passed"] for item in results if item["command"] in ("enter", "workshop", "courtyard", "chapel")),
|
||||||
|
"object_inspection": next((item["passed"] for item in results if item["command"] == "look Book of the Soul"), False),
|
||||||
|
}
|
||||||
|
out = example_eval_path(REPO_ROOT)
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
print(json.dumps({"eval_path": str(out), **summary}, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
72
scripts/evennia/generate_sample_trace.py
Normal file
72
scripts/evennia/generate_sample_trace.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(REPO_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
|
from evennia_tools.telemetry import event_log_path, session_meta_path
|
||||||
|
from evennia_tools.training import WORLD_BASICS_STEPS, example_trace_path
|
||||||
|
from scripts.evennia import evennia_mcp_server as bridge
|
||||||
|
|
||||||
|
EVENNIA_BIN = Path.home() / '.timmy' / 'evennia' / 'venv' / 'bin' / 'evennia'
|
||||||
|
GAME_DIR = Path.home() / '.timmy' / 'evennia' / 'timmy_world'
|
||||||
|
|
||||||
|
SESSION_ID = os.environ.get("TIMMY_EVENNIA_SAMPLE_SESSION_ID", "sample-evennia-world-basics")
|
||||||
|
EVAL_USERNAME = os.environ.get("TIMMY_EVENNIA_EVAL_USERNAME", "TimmyEval")
|
||||||
|
EVAL_PASSWORD = os.environ.get("TIMMY_EVENNIA_EVAL_PASSWORD", "timmy-eval-world-dev")
|
||||||
|
|
||||||
|
|
||||||
|
def reset_timmy_to_gate():
|
||||||
|
env = dict(**os.environ)
|
||||||
|
env['PYTHONPATH'] = str(REPO_ROOT) + ':' + env.get('PYTHONPATH', '')
|
||||||
|
code = f"from evennia.accounts.models import AccountDB; from evennia.accounts.accounts import DefaultAccount; from evennia.utils.search import search_object; acc=AccountDB.objects.filter(username='{EVAL_USERNAME}').first(); acc = acc or DefaultAccount.create(username='{EVAL_USERNAME}', password='{EVAL_PASSWORD}')[0]; char=list(acc.characters)[0]; gate=search_object('Gate', exact=True)[0]; char.home=gate; char.move_to(gate, quiet=True, move_hooks=False); char.save(); print('RESET_OK')"
|
||||||
|
subprocess.run([str(EVENNIA_BIN), 'shell', '-c', code], cwd=GAME_DIR, env=env, check=True, capture_output=True, text=True, timeout=120)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_to_gate() -> None:
|
||||||
|
output = bridge._observe("timmy").get("output", "")
|
||||||
|
if not output:
|
||||||
|
output = bridge._command("look", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
for _ in range(6):
|
||||||
|
if "Gate" in output:
|
||||||
|
return
|
||||||
|
if "Courtyard" in output:
|
||||||
|
output = bridge._command("gate", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
continue
|
||||||
|
if any(room in output for room in ("Workshop", "Archive", "Chapel")):
|
||||||
|
output = bridge._command("courtyard", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
continue
|
||||||
|
output = bridge._command("look", name="timmy", wait_ms=400).get("output", "")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
bridge._disconnect("timmy")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
reset_timmy_to_gate()
|
||||||
|
bridge._save_bound_session_id(SESSION_ID)
|
||||||
|
bridge._connect(name="timmy", username=EVAL_USERNAME, password=EVAL_PASSWORD)
|
||||||
|
normalize_to_gate()
|
||||||
|
for step in WORLD_BASICS_STEPS:
|
||||||
|
bridge._command(step["command"], name="timmy", wait_ms=400)
|
||||||
|
bridge._disconnect("timmy")
|
||||||
|
|
||||||
|
log_path = event_log_path(SESSION_ID)
|
||||||
|
meta_path = session_meta_path(SESSION_ID)
|
||||||
|
repo_trace = example_trace_path(REPO_ROOT)
|
||||||
|
repo_trace.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(log_path, repo_trace)
|
||||||
|
print(json.dumps({"session_id": SESSION_ID, "log_path": str(log_path), "meta_path": str(meta_path), "repo_trace": str(repo_trace)}, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
34
specs/evennia-training-data-contract.md
Normal file
34
specs/evennia-training-data-contract.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Evennia Training Data Contract
|
||||||
|
|
||||||
|
Canonical local telemetry root:
|
||||||
|
- `~/.timmy/training-data/evennia/`
|
||||||
|
|
||||||
|
Per-session layout:
|
||||||
|
- `~/.timmy/training-data/evennia/YYYYMMDD/<session_id>.jsonl`
|
||||||
|
- `~/.timmy/training-data/evennia/YYYYMMDD/<session_id>.meta.json`
|
||||||
|
|
||||||
|
Meaning:
|
||||||
|
- `<session_id>.jsonl` is the event stream emitted by the world lane
|
||||||
|
- `<session_id>.meta.json` is the sidecar mapping record tying the world trace back to the Hermes session id and event log path
|
||||||
|
|
||||||
|
Minimum event fields:
|
||||||
|
- `timestamp`
|
||||||
|
- `event`
|
||||||
|
- `actor`
|
||||||
|
- for commands: `command`
|
||||||
|
- for text output: `output` (possibly excerpted for compactness)
|
||||||
|
|
||||||
|
Canonical mapping rule:
|
||||||
|
- Hermes session id is the primary join key
|
||||||
|
- the Evennia MCP bridge receives it through `mcp_evennia_bind_session`
|
||||||
|
- all subsequent world events for that run append to the matching `<session_id>.jsonl`
|
||||||
|
- the sidecar meta file records `session_id` and `event_log_path`
|
||||||
|
|
||||||
|
Why this matters:
|
||||||
|
- Hermes transcript and Evennia world trace can be paired later for DPO curation
|
||||||
|
- the world does not become an opaque side channel
|
||||||
|
- replay/eval tools can consume the same contract
|
||||||
|
|
||||||
|
Benchmark boundary:
|
||||||
|
- Evennia traces represent the persistent-world lane
|
||||||
|
- NLE/MiniHack remains the benchmark lane
|
||||||
38
tests/test_evennia_training.py
Normal file
38
tests/test_evennia_training.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from evennia_tools.telemetry import event_log_path, session_meta_path, write_session_metadata
|
||||||
|
from evennia_tools.training import WORLD_BASICS_STEPS, example_eval_path, example_trace_path
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvenniaTraining(unittest.TestCase):
|
||||||
|
def test_world_basics_sequence_is_stable(self):
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(step["command"] for step in WORLD_BASICS_STEPS),
|
||||||
|
("look", "enter", "workshop", "look", "courtyard", "chapel", "look Book of the Soul"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_each_step_has_nonempty_expectations(self):
|
||||||
|
for step in WORLD_BASICS_STEPS:
|
||||||
|
self.assertTrue(step["expected"])
|
||||||
|
|
||||||
|
def test_example_paths_land_in_examples_dir(self):
|
||||||
|
root = Path("/tmp/repo")
|
||||||
|
self.assertEqual(example_trace_path(root).name, "world-basics-trace.example.jsonl")
|
||||||
|
self.assertEqual(example_eval_path(root).name, "world-basics-eval.example.json")
|
||||||
|
|
||||||
|
def test_session_meta_path_suffix(self):
|
||||||
|
path = session_meta_path("sess1", "/tmp/evennia-test")
|
||||||
|
self.assertEqual(path.name, "sess1.meta.json")
|
||||||
|
|
||||||
|
def test_write_session_metadata(self):
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
path = write_session_metadata("sess2", {"kind": "world"}, td)
|
||||||
|
self.assertTrue(path.exists())
|
||||||
|
self.assertIn("sess2", path.read_text())
|
||||||
|
self.assertIn(str(event_log_path("sess2", td)), path.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "look",
|
||||||
|
"expected": [
|
||||||
|
"Gate"
|
||||||
|
],
|
||||||
|
"passed": true,
|
||||||
|
"output_excerpt": "\u001b[1m\u001b[36mGate\u001b[0m\r\nA deliberate threshold into Timmy's world. The air is still here, as if entry itself matters.\r\n\u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m\r\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "enter",
|
||||||
|
"expected": [
|
||||||
|
"Courtyard"
|
||||||
|
],
|
||||||
|
"passed": true,
|
||||||
|
"output_excerpt": "\u001b[1m\u001b[36mCourtyard\u001b[0m\r\nThe central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness.\r\n\u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m\r\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "workshop",
|
||||||
|
"expected": [
|
||||||
|
"Workshop",
|
||||||
|
"Workbench"
|
||||||
|
],
|
||||||
|
"passed": true,
|
||||||
|
"output_excerpt": "\u001b[1m\u001b[36mWorkshop\u001b[0m\r\nBenches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts.\r\n\u001b[1m\u001b[37mExits:\u001b[0m courtyard\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m\r\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "look",
|
||||||
|
"expected": [
|
||||||
|
"Workshop",
|
||||||
|
"Workbench"
|
||||||
|
],
|
||||||
|
"passed": true,
|
||||||
|
"output_excerpt": "\u001b[1m\u001b[36mWorkshop\u001b[0m\r\nBenches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts.\r\n\u001b[1m\u001b[37mExits:\u001b[0m courtyard\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m\r\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "courtyard",
|
||||||
|
"expected": [
|
||||||
|
"Courtyard",
|
||||||
|
"Map Table"
|
||||||
|
],
|
||||||
|
"passed": true,
|
||||||
|
"output_excerpt": "\u001b[1m\u001b[36mCourtyard\u001b[0m\r\nThe central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness.\r\n\u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m\r\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "chapel",
|
||||||
|
"expected": [
|
||||||
|
"Chapel",
|
||||||
|
"Prayer Wall"
|
||||||
|
],
|
||||||
|
"passed": true,
|
||||||
|
"output_excerpt": "\u001b[1m\u001b[36mChapel\u001b[0m\r\nA quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried.\r\n\u001b[1m\u001b[37mExits:\u001b[0m courtyard\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m\r\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "look Book of the Soul",
|
||||||
|
"expected": [
|
||||||
|
"Book of the Soul",
|
||||||
|
"doctrinal anchor"
|
||||||
|
],
|
||||||
|
"passed": true,
|
||||||
|
"output_excerpt": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m\r\nA doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m\r\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orientation": true,
|
||||||
|
"navigation": true,
|
||||||
|
"object_inspection": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:10:27.151397+00:00"}
|
||||||
|
{"event": "connect", "actor": "Timmy", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect <username> <password>\u001b[0m If you n...", "timestamp": "2026-03-28T19:10:29.161065+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:10:29.566853+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "enter", "output": "Command 'enter' is not available. Maybe you meant \"chardelete\" or \"emote\"?\u001b[0m", "timestamp": "2026-03-28T19:10:29.969554+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "workshop", "output": "Command 'workshop' is not available. Maybe you meant \"who\"?\u001b[0m", "timestamp": "2026-03-28T19:10:30.375076+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:10:30.776821+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:10:31.182522+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:10:31.588268+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:10:31.993857+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:10:31.994155+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:12:41.096394+00:00"}
|
||||||
|
{"event": "connect", "actor": "Timmy", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect <username> <password>\u001b[0m If you n...", "timestamp": "2026-03-28T19:12:43.109101+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:12:43.514836+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "enter", "output": "Command 'enter' is not available. Maybe you meant \"chardelete\" or \"emote\"?\u001b[0m", "timestamp": "2026-03-28T19:12:43.920420+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "workshop", "output": "Command 'workshop' is not available. Maybe you meant \"who\"?\u001b[0m", "timestamp": "2026-03-28T19:12:44.323687+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:12:44.729334+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:12:45.134875+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:12:45.535855+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:12:45.941394+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:12:45.941887+00:00"}
|
||||||
|
{"event": "connect", "actor": "Timmy", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect <username> <password>\u001b[0m If you n...", "timestamp": "2026-03-28T19:14:18.509613+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:14:18.916211+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "enter", "output": "Command 'enter' is not available. Maybe you meant \"chardelete\" or \"emote\"?\u001b[0m", "timestamp": "2026-03-28T19:14:19.319885+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "workshop", "output": "Command 'workshop' is not available. Maybe you meant \"who\"?\u001b[0m", "timestamp": "2026-03-28T19:14:19.720596+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:14:20.126229+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:14:20.529991+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:14:20.935654+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:14:21.341176+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:14:21.341799+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:14:22.231687+00:00"}
|
||||||
|
{"event": "connect", "actor": "TimmyEval", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect <username> <password>\u001b[0m If you n...", "timestamp": "2026-03-28T19:17:31.862892+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mGate\u001b[0m A deliberate threshold into Timmy's world. The air is still here, as if entry itself matters. \u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m", "timestamp": "2026-03-28T19:17:32.269727+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "enter", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:17:32.675348+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "workshop", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:17:33.078265+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:17:33.484067+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:17:33.889665+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:17:34.295231+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:17:34.700773+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:17:34.701330+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:17:35.654223+00:00"}
|
||||||
|
{"event": "connect", "actor": "TimmyEval", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect <username> <password>\u001b[0m If you n...", "timestamp": "2026-03-28T19:31:52.782015+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:31:53.394240+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:31:53.796861+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "gate", "output": "\u001b[1m\u001b[36mGate\u001b[0m A deliberate threshold into Timmy's world. The air is still here, as if entry itself matters. \u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m", "timestamp": "2026-03-28T19:31:54.202470+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mGate\u001b[0m A deliberate threshold into Timmy's world. The air is still here, as if entry itself matters. \u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m", "timestamp": "2026-03-28T19:31:54.605358+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "enter", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:31:55.007358+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "workshop", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:31:55.409107+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:31:55.814849+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:31:56.220756+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:31:56.626349+00:00"}
|
||||||
|
{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:31:57.029105+00:00"}
|
||||||
|
{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:31:57.029536+00:00"}
|
||||||
Reference in New Issue
Block a user