stable
This commit is contained in:
parent
fbdac9ebf1
commit
86226ccc94
97 changed files with 5790 additions and 2307 deletions
|
|
@ -281,9 +281,10 @@ def check_runtime_diagnostics_hooks() -> None:
|
|||
debug_tools = read_text(ROOT / "js/debug_tools.js")
|
||||
world_update = read_text(ROOT / "js/world_update.js")
|
||||
world_update_phases = read_text(ROOT / "js/world_update_phases.js")
|
||||
simulation_systems = read_text(ROOT / "js/simulation_systems.js")
|
||||
if "runtimeDiagnostics()" not in spatial_budget:
|
||||
fail("World.runtimeDiagnostics hook missing")
|
||||
if "lastRuntimeDiagnostics" not in (world_update + world_update_phases):
|
||||
if "lastRuntimeDiagnostics" not in (world_update + world_update_phases + simulation_systems):
|
||||
fail("world update phases do not publish lastRuntimeDiagnostics")
|
||||
if "spatial rebuild" not in debug_tools or "behavior forced" not in debug_tools:
|
||||
fail("debug overlay is missing runtime diagnostic lines")
|
||||
|
|
@ -359,7 +360,7 @@ def check_item_bucket_polish() -> None:
|
|||
fail("debug overlay is missing item bucket/id-map diagnostics")
|
||||
routed_files = {
|
||||
"js/world_placement_log.js": "addItem?.(item",
|
||||
"js/world_update_phases.js": "addItem?.(drop",
|
||||
"js/simulation_ambient_system.js": "addItem?.(drop",
|
||||
"js/world_combat_effects.js": "addItem?.(it",
|
||||
"js/ants.js": "addItem?.(corpse",
|
||||
"js/tarinai_social_move_life.js": "addItem?.(new Item(\"trace\"",
|
||||
|
|
@ -503,6 +504,14 @@ def check_phase_and_input_split() -> None:
|
|||
data = manifest()
|
||||
js_files = data["js"]
|
||||
required = [
|
||||
"js/simulation_runtime_helpers.js",
|
||||
"js/simulation_environment_system.js",
|
||||
"js/simulation_item_ant_system.js",
|
||||
"js/simulation_effects_system.js",
|
||||
"js/simulation_creature_system.js",
|
||||
"js/simulation_maintenance_system.js",
|
||||
"js/simulation_ambient_system.js",
|
||||
"js/simulation_systems.js",
|
||||
"js/world_update_phases.js",
|
||||
"js/ui_input_shared.js",
|
||||
"js/ui_input_touch.js",
|
||||
|
|
@ -517,10 +526,11 @@ def check_phase_and_input_split() -> None:
|
|||
touch = read_text(ROOT / "js/ui_input_touch.js")
|
||||
mouse = read_text(ROOT / "js/ui_input_mouse.js")
|
||||
shared = read_text(ROOT / "js/ui_input_shared.js")
|
||||
if "TarinaiWorldUpdatePhases" not in world_update or "function update(worldRef, dt)" not in phases:
|
||||
systems = read_text(ROOT / "js/simulation_systems.js")
|
||||
if "TarinaiSimulation" not in world_update or "function update(worldRef, dt)" not in phases:
|
||||
fail("World.update is not delegated to world_update_phases.js")
|
||||
for token in ["updateClock", "updateEnvironment", "updateItemsAndAnts", "updateCreatures", "runMaintenance", "spawnRainWater"]:
|
||||
if token not in phases:
|
||||
if token not in phases or token not in systems:
|
||||
fail(f"world update phase missing: {token}")
|
||||
if "touchstart" in ui_bind or "mousedown" in ui_bind or "touchmove" in ui_bind:
|
||||
fail("ui_bind.js still owns field pointer input handlers")
|
||||
|
|
@ -591,6 +601,625 @@ def check_processing_split() -> None:
|
|||
fail(f"tarinai_needs_items.js still owns extracted processing: {token}")
|
||||
ok("item and Tarinai processing split present")
|
||||
|
||||
|
||||
def command_dispatcher_smoke() -> None:
|
||||
source = read_text(ROOT / "js/command_dispatcher.js")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
context.World = class World {{}};
|
||||
context.scalableToolIds = () => ['grass', 'sweet'];
|
||||
context.normalizedToolSizeFor = (world, tool) => (world.toolSizes && world.toolSizes[tool]) || world.toolSize || 'medium';
|
||||
vm.createContext(context);
|
||||
vm.runInContext({source!r}, context, {{ filename: 'command_dispatcher.js' }});
|
||||
const events = [];
|
||||
const world = new context.World();
|
||||
world.tool = 'observe';
|
||||
world.toolSize = 'medium';
|
||||
world.toolSizes = {{}};
|
||||
world.speed = 1;
|
||||
world.paused = false;
|
||||
world.w = 1000;
|
||||
world.h = 700;
|
||||
world.viewportW = 500;
|
||||
world.viewportH = 300;
|
||||
world.clampCamera = () => {{ world.clamped = true; }};
|
||||
world.emit = (type, payload) => events.push([type, payload]);
|
||||
world.handleClick = (x, y) => {{ world.clicked = [x, y]; }};
|
||||
const target = {{ x: 420, y: 260, dead: false }};
|
||||
if (!context.TarinaiCommands.dispatch(world, {{ type: 'tool.select', toolId: 'grass' }}).ok || world.tool !== 'grass') throw new Error('tool select failed');
|
||||
if (!context.TarinaiCommands.dispatch(world, {{ type: 'tool.size.cycle', toolId: 'grass', order: ['small', 'medium', 'large'] }}).ok || world.toolSize !== 'large') throw new Error('tool size failed');
|
||||
if (!context.TarinaiCommands.dispatch(world, {{ type: 'selection.focus', target }}).ok || world.selected !== target || world.cameraX !== 170 || world.cameraY !== 110) throw new Error('focus failed');
|
||||
if (!context.TarinaiCommands.dispatch(world, {{ type: 'selection.favorite.toggle' }}).ok || target.favorite !== true) throw new Error('favorite failed');
|
||||
if (!context.TarinaiCommands.dispatch(world, {{ type: 'simulation.paused.toggle' }}).ok || world.paused !== true) throw new Error('pause failed');
|
||||
if (!context.TarinaiCommands.dispatch(world, {{ type: 'simulation.speed.cycle', speeds: [1, 2, 4] }}).ok || world.speed !== 2) throw new Error('speed failed');
|
||||
if (!context.TarinaiCommands.dispatch(world, {{ type: 'pointer.primary', x: 7, y: 9 }}).ok || world.clicked[0] !== 7 || world.clicked[1] !== 9) throw new Error('pointer failed');
|
||||
if (typeof world.dispatchCommand !== 'function' || !world.dispatchCommand({{ type: 'selection.clear' }}).ok || world.selected !== null) throw new Error('World facade failed');
|
||||
if (!events.some(([type]) => type === 'selection:changed')) throw new Error('selection event missing');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("command dispatcher smoke test failed")
|
||||
ok("command dispatcher smoke test passed")
|
||||
|
||||
|
||||
def check_system_order_boundary() -> None:
|
||||
data = manifest()
|
||||
js_files = data["js"]
|
||||
required = [
|
||||
"js/simulation_runtime_helpers.js",
|
||||
"js/simulation_environment_system.js",
|
||||
"js/simulation_item_ant_system.js",
|
||||
"js/simulation_effects_system.js",
|
||||
"js/simulation_creature_system.js",
|
||||
"js/simulation_maintenance_system.js",
|
||||
"js/simulation_ambient_system.js",
|
||||
"js/simulation_systems.js",
|
||||
"js/world_update_phases.js",
|
||||
"js/system_order.js",
|
||||
"js/simulation.js",
|
||||
"js/world_update.js",
|
||||
]
|
||||
for rel in required:
|
||||
if rel not in js_files:
|
||||
fail(f"simulation boundary module missing from manifest: {rel}")
|
||||
split_order = [
|
||||
"js/simulation_runtime_helpers.js",
|
||||
"js/simulation_environment_system.js",
|
||||
"js/simulation_item_ant_system.js",
|
||||
"js/simulation_effects_system.js",
|
||||
"js/simulation_creature_system.js",
|
||||
"js/simulation_maintenance_system.js",
|
||||
"js/simulation_ambient_system.js",
|
||||
"js/simulation_systems.js",
|
||||
"js/world_update_phases.js",
|
||||
"js/system_order.js",
|
||||
"js/simulation.js",
|
||||
"js/world_update.js",
|
||||
]
|
||||
if [rel for rel in js_files if rel in split_order] != split_order:
|
||||
fail("simulation boundary load order is invalid")
|
||||
system_order = read_text(ROOT / "js/system_order.js")
|
||||
simulation = read_text(ROOT / "js/simulation.js")
|
||||
systems_source = read_text(ROOT / "js/simulation_systems.js")
|
||||
if "TarinaiSimulationSystems" not in systems_source or "const phases = Object.freeze" not in systems_source:
|
||||
fail("concrete simulation systems facade missing exported phase registry")
|
||||
concrete_modules = {
|
||||
"js/simulation_runtime_helpers.js": "TarinaiSimulationRuntime",
|
||||
"js/simulation_environment_system.js": "TarinaiEnvironmentSimulationSystem",
|
||||
"js/simulation_item_ant_system.js": "TarinaiItemAntSimulationSystem",
|
||||
"js/simulation_effects_system.js": "TarinaiEffectsSimulationSystem",
|
||||
"js/simulation_creature_system.js": "TarinaiCreatureSimulationSystem",
|
||||
"js/simulation_maintenance_system.js": "TarinaiMaintenanceSimulationSystem",
|
||||
"js/simulation_ambient_system.js": "TarinaiAmbientSimulationSystem",
|
||||
}
|
||||
for rel, token in concrete_modules.items():
|
||||
if token not in read_text(ROOT / rel):
|
||||
fail(f"concrete simulation split module missing export: {rel}")
|
||||
if "TarinaiSimulationSystems" not in read_text(ROOT / "js/world_update_phases.js"):
|
||||
fail("world_update_phases.js is not delegating to the concrete simulation systems module")
|
||||
for token in ["const SYSTEM_ORDER", "clock", "environment", "items_and_ants", "creatures", "maintenance", "diagnostics"]:
|
||||
if token not in system_order:
|
||||
fail(f"system order token missing: {token}")
|
||||
if "TarinaiSystemOrder" not in simulation or "systemOrder" not in simulation:
|
||||
fail("simulation.js does not expose the named system order boundary")
|
||||
ok("named simulation system order present")
|
||||
|
||||
|
||||
def simulation_boundary_smoke() -> None:
|
||||
sources = {rel: read_text(ROOT / rel) for rel in [
|
||||
"js/simulation_runtime_helpers.js",
|
||||
"js/simulation_environment_system.js",
|
||||
"js/simulation_item_ant_system.js",
|
||||
"js/simulation_effects_system.js",
|
||||
"js/simulation_creature_system.js",
|
||||
"js/simulation_maintenance_system.js",
|
||||
"js/simulation_ambient_system.js",
|
||||
"js/simulation_systems.js",
|
||||
"js/world_update_phases.js",
|
||||
"js/system_order.js",
|
||||
"js/simulation.js",
|
||||
]}
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
vm.createContext(context);
|
||||
for (const [filename, source] of Object.entries({sources!r})) {{
|
||||
vm.runInContext(source, context, {{ filename }});
|
||||
}}
|
||||
if (!context.TarinaiSimulationSystems?.phases?.updateClock) throw new Error('simulation systems missing');
|
||||
if (!context.TarinaiWorldUpdatePhases?.phases?.updateClock) throw new Error('phase facade missing');
|
||||
if (!context.TarinaiSystemOrder?.names?.includes('clock')) throw new Error('system order missing');
|
||||
if (!context.TarinaiSimulation?.systemOrder) throw new Error('simulation facade missing');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("simulation boundary smoke test failed")
|
||||
ok("simulation boundary smoke test passed")
|
||||
|
||||
|
||||
|
||||
def check_update_policy_boundaries() -> None:
|
||||
data = manifest()
|
||||
js_files = data["js"]
|
||||
required = ["js/item_update_policy.js", "js/tarinai_update_policy.js"]
|
||||
for rel in required:
|
||||
if rel not in js_files:
|
||||
fail(f"update policy boundary module missing from manifest: {rel}")
|
||||
if not (js_files.index("js/item_lifecycle_runtime.js") < js_files.index("js/item_update_policy.js") < js_files.index("js/item_runtime.js")):
|
||||
fail("item update policy load order is invalid")
|
||||
if not (js_files.index("js/simulation_runtime_helpers.js") < js_files.index("js/tarinai_update_policy.js") < js_files.index("js/simulation_creature_system.js")):
|
||||
fail("Tarinai update policy load order is invalid")
|
||||
item_policy = read_text(ROOT / "js/item_update_policy.js")
|
||||
tarinai_policy = read_text(ROOT / "js/tarinai_update_policy.js")
|
||||
scheduler = read_text(ROOT / "js/item_update_scheduler.js")
|
||||
sim_helpers = read_text(ROOT / "js/simulation_runtime_helpers.js")
|
||||
creature_system = read_text(ROOT / "js/simulation_creature_system.js")
|
||||
required_tokens = [
|
||||
(item_policy, "global.TarinaiItemUpdatePolicy"),
|
||||
(item_policy, "itemUpdateInterval"),
|
||||
(item_policy, "forScheduledItems"),
|
||||
(tarinai_policy, "global.TarinaiCreatureUpdatePolicy"),
|
||||
(tarinai_policy, "updateInterval"),
|
||||
(scheduler, "TarinaiItemUpdatePolicy"),
|
||||
(sim_helpers, "TarinaiItemUpdatePolicy"),
|
||||
(creature_system, "TarinaiCreatureUpdatePolicy"),
|
||||
]
|
||||
for source, token in required_tokens:
|
||||
if token not in source:
|
||||
fail(f"update policy boundary token missing: {token}")
|
||||
duplicated_item_policy_sources = [scheduler, sim_helpers]
|
||||
for source in duplicated_item_policy_sources:
|
||||
if "const REALTIME_ITEM_TYPES" in source or "const PIN_ITEM_TYPES" in source or "SCHEDULED_ITEM_TYPES =" in source:
|
||||
fail("item update policy constants are duplicated outside item_update_policy.js")
|
||||
if "function tarinaiUpdateInterval" in creature_system:
|
||||
fail("Tarinai update cadence still lives inside simulation_creature_system.js")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
context.isServingFoodType = (type) => type === 'food';
|
||||
context.TarinaiItemRuntime = {{
|
||||
updateOne(item, dt) {{ item.runs = (item.runs || 0) + 1; item.lastDt = dt; return true; }}
|
||||
}};
|
||||
vm.createContext(context);
|
||||
for (const file of ['js/item_update_policy.js', 'js/item_update_scheduler.js']) {{
|
||||
vm.runInContext(fs.readFileSync(file, 'utf8'), context, {{ filename: file }});
|
||||
}}
|
||||
const world = {{ time: 0, items: [{{ type: 'ball' }}, {{ type: 'food' }}, {{ type: 'grass' }}], ensureItemBuckets() {{}}, itemBucketRebuildsTotal: 0 }};
|
||||
if (context.TarinaiItemUpdateScheduler.run(world, 0.1) < 1) throw new Error('realtime item did not run');
|
||||
world.time = 5.1;
|
||||
if (context.TarinaiItemUpdateScheduler.run(world, 0.1) < 1) throw new Error('scheduled item did not run');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("update policy scheduler smoke test failed")
|
||||
ok("update policy boundaries present")
|
||||
|
||||
|
||||
def check_entity_runtime_boundary() -> None:
|
||||
data = manifest()
|
||||
js_files = data["js"]
|
||||
required = ["js/item_runtime.js", "js/tarinai_runtime.js"]
|
||||
for rel in required:
|
||||
if rel not in js_files:
|
||||
fail(f"entity runtime boundary module missing from manifest: {rel}")
|
||||
if not (js_files.index("js/item_lifecycle_runtime.js") < js_files.index("js/item_runtime.js") < js_files.index("js/structure_lifecycle.js")):
|
||||
fail("item runtime boundary load order is invalid")
|
||||
if not (js_files.index("js/tarinai_needs_items.js") < js_files.index("js/tarinai_social_move_life.js") < js_files.index("js/tarinai_runtime.js") < js_files.index("js/tarinai_render.js")):
|
||||
fail("Tarinai runtime boundary load order is invalid")
|
||||
item_runtime = read_text(ROOT / "js/item_runtime.js")
|
||||
tarinai_runtime = read_text(ROOT / "js/tarinai_runtime.js")
|
||||
item_scheduler = read_text(ROOT / "js/item_update_scheduler.js")
|
||||
sim_helpers = read_text(ROOT / "js/simulation_runtime_helpers.js")
|
||||
creature_system = read_text(ROOT / "js/simulation_creature_system.js")
|
||||
required_tokens = [
|
||||
(item_runtime, "global.TarinaiItemRuntime"),
|
||||
(item_runtime, "installPrototypeBoundary"),
|
||||
(item_runtime, "updateScheduled"),
|
||||
(tarinai_runtime, "global.TarinaiCreatureRuntime"),
|
||||
(tarinai_runtime, "updateBatch"),
|
||||
(item_scheduler, "TarinaiItemRuntime?.updateOne"),
|
||||
(sim_helpers, "TarinaiItemRuntime?.updateScheduled"),
|
||||
(creature_system, "TarinaiCreatureRuntime?.updateOne"),
|
||||
]
|
||||
for source, token in required_tokens:
|
||||
if token not in source:
|
||||
fail(f"entity runtime boundary token missing: {token}")
|
||||
ok("entity update runtime boundary present")
|
||||
|
||||
|
||||
def check_tarinai_update_pipeline_boundary() -> None:
|
||||
data = manifest()
|
||||
js_files = data["js"]
|
||||
required = [
|
||||
"js/tarinai_sunbath_system.js",
|
||||
"js/tarinai_cursor_care_system.js",
|
||||
"js/tarinai_local_environment_system.js",
|
||||
"js/tarinai_update_legacy_system.js",
|
||||
"js/tarinai_forced_action_planner_system.js",
|
||||
"js/tarinai_action_planner_system.js",
|
||||
"js/tarinai_need_planner_system.js",
|
||||
"js/tarinai_item_interaction_context.js",
|
||||
"js/tarinai_food_interaction_system.js",
|
||||
"js/tarinai_contact_item_system.js",
|
||||
"js/tarinai_item_interaction_system.js",
|
||||
"js/tarinai_update_step_frame.js",
|
||||
"js/tarinai_update_step_ai.js",
|
||||
"js/tarinai_update_step_environment.js",
|
||||
"js/tarinai_update_step_movement.js",
|
||||
"js/tarinai_update_step_health.js",
|
||||
"js/tarinai_update_step_legacy.js",
|
||||
"js/tarinai_update_pipeline.js",
|
||||
]
|
||||
for rel in required:
|
||||
if rel not in js_files:
|
||||
fail(f"Tarinai update pipeline module missing from manifest: {rel}")
|
||||
expected_order = [
|
||||
"js/tarinai_needs_items.js",
|
||||
"js/tarinai_forced_action_planner_system.js",
|
||||
"js/tarinai_action_planner_system.js",
|
||||
"js/tarinai_need_planner_system.js",
|
||||
"js/tarinai_item_interaction_context.js",
|
||||
"js/tarinai_food_interaction_system.js",
|
||||
"js/tarinai_contact_item_system.js",
|
||||
"js/tarinai_item_interaction_system.js",
|
||||
"js/tarinai_sunbath_system.js",
|
||||
"js/tarinai_cursor_care_system.js",
|
||||
"js/tarinai_local_environment_system.js",
|
||||
"js/tarinai_update_legacy_system.js",
|
||||
"js/tarinai_social_move_life.js",
|
||||
"js/tarinai_update_step_frame.js",
|
||||
"js/tarinai_update_step_ai.js",
|
||||
"js/tarinai_update_step_environment.js",
|
||||
"js/tarinai_update_step_movement.js",
|
||||
"js/tarinai_update_step_health.js",
|
||||
"js/tarinai_update_step_legacy.js",
|
||||
"js/tarinai_update_pipeline.js",
|
||||
"js/tarinai_runtime.js",
|
||||
]
|
||||
if [rel for rel in js_files if rel in expected_order] != expected_order:
|
||||
fail("Tarinai update pipeline load order is invalid")
|
||||
exports = {
|
||||
"js/tarinai_sunbath_system.js": "global.TarinaiSunbathSystem",
|
||||
"js/tarinai_cursor_care_system.js": "global.TarinaiCursorCareSystem",
|
||||
"js/tarinai_local_environment_system.js": "global.TarinaiLocalEnvironmentSystem",
|
||||
"js/tarinai_update_legacy_system.js": "global.TarinaiLegacyUpdateSystem",
|
||||
"js/tarinai_forced_action_planner_system.js": "global.TarinaiForcedActionPlannerSystem",
|
||||
"js/tarinai_action_planner_system.js": "global.TarinaiActionPlannerSystem",
|
||||
"js/tarinai_need_planner_system.js": "global.TarinaiNeedPlannerSystem",
|
||||
"js/tarinai_item_interaction_context.js": "global.TarinaiItemInteractionContext",
|
||||
"js/tarinai_food_interaction_system.js": "global.TarinaiFoodInteractionSystem",
|
||||
"js/tarinai_contact_item_system.js": "global.TarinaiContactItemSystem",
|
||||
"js/tarinai_item_interaction_system.js": "global.TarinaiItemInteractionSystem",
|
||||
"js/tarinai_update_step_frame.js": "global.TarinaiFrameUpdateStep",
|
||||
"js/tarinai_update_step_ai.js": "global.TarinaiAiUpdateStep",
|
||||
"js/tarinai_update_step_environment.js": "global.TarinaiEnvironmentUpdateStep",
|
||||
"js/tarinai_update_step_movement.js": "global.TarinaiMovementUpdateStep",
|
||||
"js/tarinai_update_step_health.js": "global.TarinaiHealthUpdateStep",
|
||||
"js/tarinai_update_step_legacy.js": "global.TarinaiLegacyUpdateStep",
|
||||
"js/tarinai_update_pipeline.js": "global.TarinaiUpdatePipeline",
|
||||
}
|
||||
for rel, token in exports.items():
|
||||
if token not in read_text(ROOT / rel):
|
||||
fail(f"Tarinai update pipeline export missing: {rel}")
|
||||
runtime = read_text(ROOT / "js/tarinai_runtime.js")
|
||||
if "TarinaiUpdatePipeline.updateOne" not in runtime or "legacyUpdate: fn" not in runtime:
|
||||
fail("Tarinai runtime does not route through TarinaiUpdatePipeline with legacy fallback")
|
||||
|
||||
needs_items = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||
legacy_system = read_text(ROOT / "js/tarinai_update_legacy_system.js")
|
||||
sunbath_system = read_text(ROOT / "js/tarinai_sunbath_system.js")
|
||||
cursor_system = read_text(ROOT / "js/tarinai_cursor_care_system.js")
|
||||
local_env_system = read_text(ROOT / "js/tarinai_local_environment_system.js")
|
||||
interaction_system = read_text(ROOT / "js/tarinai_item_interaction_system.js")
|
||||
if "TarinaiLegacyUpdateSystem.updateOne" not in needs_items or "const minute = dt / 60" in needs_items:
|
||||
fail("Tarinai.prototype.update is not a thin facade over the update pipeline/legacy system")
|
||||
if "TarinaiItemInteractionSystem?.updateOne" not in needs_items or "const foodNeed = Number(this.needRaw?.food" in needs_items:
|
||||
fail("Tarinai.prototype.interactWithItems is not a thin facade over the interaction system")
|
||||
for facade_token in ["TarinaiSunbathSystem?.updateOne", "TarinaiCursorCareSystem?.updateOne", "TarinaiLocalEnvironmentSystem?.updateOne"]:
|
||||
if facade_token not in needs_items:
|
||||
fail(f"Tarinai prototype subsystem facade missing: {facade_token}")
|
||||
for token in ["const minute = dt / 60", "this.resolveNeeds(needStepDt)", "this.applyEnvironment(envDt)", "this.checkLife(dt)"]:
|
||||
if token not in legacy_system:
|
||||
fail(f"Tarinai legacy update system missing extracted monolith token: {token}")
|
||||
interaction_context = read_text(ROOT / "js/tarinai_item_interaction_context.js")
|
||||
food_interaction = read_text(ROOT / "js/tarinai_food_interaction_system.js")
|
||||
contact_interaction = read_text(ROOT / "js/tarinai_contact_item_system.js")
|
||||
for source, token in [
|
||||
(sunbath_system, "this.sunbathFrameTimer"),
|
||||
(cursor_system, "this.nextCursorHeartAt"),
|
||||
(local_env_system, "grassComfort"),
|
||||
(interaction_context, "const foodNeed = Number(tarinai.needRaw?.food"),
|
||||
(food_interaction, "tarinai.applyParamItemEffect"),
|
||||
(contact_interaction, "tarinai.applyWaterEffect(dt, it.type)"),
|
||||
]:
|
||||
if token not in source:
|
||||
fail(f"Tarinai update subsystem missing extracted token: {token}")
|
||||
if "TarinaiItemInteractionContext?.create" not in interaction_system or "TarinaiFoodInteractionSystem?.updateCandidate" not in interaction_system or "TarinaiContactItemSystem?.updateCandidate" not in interaction_system:
|
||||
fail("Tarinai item interaction orchestrator does not delegate to context/food/contact candidate subsystems")
|
||||
if "for (const item of ctx.foodCandidates" not in interaction_system:
|
||||
fail("Tarinai item interaction orchestrator does not preserve per-candidate interaction order")
|
||||
if "TarinaiNeedPlannerSystem?.updateOne" not in needs_items or "function resolveNeedsLegacy" not in needs_items:
|
||||
fail("Tarinai resolveNeeds is not routed through planner system with legacy fallback")
|
||||
if "TarinaiNeedPlannerLegacySystem" not in needs_items:
|
||||
fail("Tarinai need planner legacy system export missing")
|
||||
if "TarinaiItemInteractionSystem.updateOne" not in read_text(ROOT / "js/tarinai_update_step_ai.js"):
|
||||
fail("Tarinai AI step does not delegate item interactions to subsystem")
|
||||
if "TarinaiCursorCareSystem.updateOne" not in read_text(ROOT / "js/tarinai_update_step_frame.js"):
|
||||
fail("Tarinai frame step does not delegate cursor care to subsystem")
|
||||
if "TarinaiLocalEnvironmentSystem.updateOne" not in read_text(ROOT / "js/tarinai_update_step_environment.js"):
|
||||
fail("Tarinai environment step does not delegate local environment to subsystem")
|
||||
if "TarinaiSunbathSystem.updateOne" not in read_text(ROOT / "js/tarinai_update_step_movement.js"):
|
||||
fail("Tarinai movement step does not delegate sunbath to subsystem")
|
||||
|
||||
pipeline = read_text(ROOT / "js/tarinai_update_pipeline.js")
|
||||
for token in ["TarinaiFrameUpdateStep", "TarinaiAiUpdateStep", "TarinaiEnvironmentUpdateStep", "TarinaiMovementUpdateStep", "TarinaiHealthUpdateStep", "TarinaiLegacyUpdateStep"]:
|
||||
if token not in pipeline:
|
||||
fail(f"Tarinai update pipeline missing step token: {token}")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
context.CONFIG = {{ hungerPerMinute: 60, lonelinessPerMinute: 60, energyPerMinute: 60 }};
|
||||
context.clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
||||
context.rand = (a, b) => (a + b) / 2;
|
||||
context.pick = (arr) => arr[0];
|
||||
context.stableUnit = () => 0.5;
|
||||
context.applyNeedShock = () => undefined;
|
||||
context.applyNeedRelief = () => undefined;
|
||||
context.calculateStressFromNeeds = () => 0;
|
||||
context.applyGroundStressModifier = (t, v) => v;
|
||||
context.createDefaultNeeds = () => ({{ food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 }});
|
||||
context.tarinaiMaxEnergy = () => 100;
|
||||
context.tarinaiMetabolicHungerScale = () => 1;
|
||||
vm.createContext(context);
|
||||
for (const file of {required!r}) {{
|
||||
vm.runInContext(fs.readFileSync(file, 'utf8'), context, {{ filename: file }});
|
||||
}}
|
||||
const calls = [];
|
||||
const world = {{
|
||||
time: 1,
|
||||
groundType: 'soil',
|
||||
weather: 'cloudy',
|
||||
colonyMood: {{ id: 'relaxed' }},
|
||||
selected: null,
|
||||
spendScheduledWork() {{ return true; }},
|
||||
temperatureAt() {{ return 0.62; }},
|
||||
lightLevel() {{ return 0.5; }},
|
||||
spawnBleedEffect() {{ calls.push('bleed'); }},
|
||||
resolveFenceCollision() {{ calls.push('fence'); }},
|
||||
}};
|
||||
const t = {{
|
||||
world, dead: false, x: 10, y: 12, vx: 0, vy: 0, radius: 20,
|
||||
age: 0, reproductionTimer: 10, blink: 0, eatTimer: 0, eatCooldown: 0, foodReactTimer: 0,
|
||||
surpriseTimer: 0, fearTimer: 0, intimidateTimer: 0, intimidatedTimer: 0, stress: 0,
|
||||
hpBarTimer: 0, stressBarTimer: 0, loveMochiTimer: 0, fightMochiTimer: 0,
|
||||
birthRitualTimer: 0, pokeFlashTimer: 0, cursorPetting: 0, pinchThoughtTimer: 0,
|
||||
sunbathCooldown: 0, fightTimer: 0, fightCooldown: 0, defeatedTimer: 0, stretchTimer: 0,
|
||||
grassEatTimer: 0, hurtTimer: 0, fallTimer: 0, tempTimer: 0, tempComfort: 0.62,
|
||||
hunger: 10, loneliness: 0, energy: 100, trait: {{ hunger: 1, social: 1, sleep: 1 }},
|
||||
state: 'idle', aiTimer: 0, envTimer: 0, needs: context.createDefaultNeeds(), needRaw: context.createDefaultNeeds(),
|
||||
updateZunchiDisease() {{ calls.push('zunchiDisease'); }},
|
||||
updateSpecialDiseases() {{ calls.push('specialDisease'); }},
|
||||
freezeSleepDiseaseMotion() {{ return false; }},
|
||||
applyCursorContactCare() {{ calls.push('cursorCare'); }},
|
||||
personalityProfile() {{ return {{ fear: 1 }}; }},
|
||||
updateFacing() {{ calls.push('facing'); }},
|
||||
updateSunbath() {{ calls.push('sunbath'); }},
|
||||
maintainTargetProgress() {{ calls.push('targetProgress'); }},
|
||||
move() {{ calls.push('move'); }},
|
||||
updateNestBoxPresence() {{ calls.push('nestBox'); }},
|
||||
checkLife() {{ calls.push('checkLife'); }},
|
||||
}};
|
||||
if (!context.TarinaiUpdatePipeline.canRunConcretePipeline()) throw new Error('pipeline cannot run concrete steps');
|
||||
if (!context.TarinaiUpdatePipeline.updateOne(t, 0.016, {{ legacyUpdate() {{ throw new Error('legacy should not run'); }} }})) throw new Error('pipeline update failed');
|
||||
for (const expected of ['zunchiDisease', 'specialDisease', 'facing', 'targetProgress', 'move', 'nestBox', 'fence', 'checkLife']) {{
|
||||
if (!calls.includes(expected)) throw new Error('missing pipeline call: ' + expected);
|
||||
}}
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("Tarinai update pipeline smoke test failed")
|
||||
ok("Tarinai update pipeline boundary present")
|
||||
|
||||
|
||||
|
||||
def check_item_lifecycle_pipeline_boundary() -> None:
|
||||
data = manifest()
|
||||
js_files = data["js"]
|
||||
dynamic_required = [
|
||||
"js/item_dynamic_tool_system.js",
|
||||
"js/item_dynamic_ball_system.js",
|
||||
"js/item_dynamic_duplicator_system.js",
|
||||
"js/item_dynamic_pin_system.js",
|
||||
"js/item_dynamic_zunchi_system.js",
|
||||
"js/item_dynamic_system.js",
|
||||
]
|
||||
required = [
|
||||
"js/item_lifecycle_support.js",
|
||||
"js/item_lifecycle_legacy_system.js",
|
||||
*dynamic_required,
|
||||
"js/item_lifecycle_decay_system.js",
|
||||
"js/item_lifecycle_growth_system.js",
|
||||
"js/item_lifecycle_step_frame.js",
|
||||
"js/item_lifecycle_step_decay.js",
|
||||
"js/item_lifecycle_step_dynamic.js",
|
||||
"js/item_lifecycle_step_growth.js",
|
||||
"js/item_lifecycle_step_legacy.js",
|
||||
"js/item_lifecycle_pipeline.js",
|
||||
]
|
||||
for rel in required:
|
||||
if rel not in js_files:
|
||||
fail(f"Item lifecycle pipeline module missing from manifest: {rel}")
|
||||
expected_order = [
|
||||
"js/item_lifecycle_support.js",
|
||||
"js/item_lifecycle_legacy_system.js",
|
||||
"js/item_lifecycle_runtime.js",
|
||||
"js/item_update_policy.js",
|
||||
*dynamic_required,
|
||||
"js/item_lifecycle_decay_system.js",
|
||||
"js/item_lifecycle_growth_system.js",
|
||||
"js/item_lifecycle_step_frame.js",
|
||||
"js/item_lifecycle_step_decay.js",
|
||||
"js/item_lifecycle_step_dynamic.js",
|
||||
"js/item_lifecycle_step_growth.js",
|
||||
"js/item_lifecycle_step_legacy.js",
|
||||
"js/item_lifecycle_pipeline.js",
|
||||
"js/item_runtime.js",
|
||||
]
|
||||
if [rel for rel in js_files if rel in expected_order] != expected_order:
|
||||
fail("Item lifecycle pipeline load order is invalid")
|
||||
exports = {
|
||||
"js/item_lifecycle_support.js": "global.TarinaiItemLifecycleRuntimeSupport",
|
||||
"js/item_lifecycle_legacy_system.js": "global.TarinaiItemLifecycleLegacySystem",
|
||||
"js/item_dynamic_tool_system.js": "global.TarinaiItemDynamicToolSystem",
|
||||
"js/item_dynamic_ball_system.js": "global.TarinaiItemDynamicBallSystem",
|
||||
"js/item_dynamic_duplicator_system.js": "global.TarinaiItemDynamicDuplicatorSystem",
|
||||
"js/item_dynamic_pin_system.js": "global.TarinaiItemDynamicPinSystem",
|
||||
"js/item_dynamic_zunchi_system.js": "global.TarinaiItemDynamicZunchiSystem",
|
||||
"js/item_dynamic_system.js": "global.TarinaiItemDynamicSystem",
|
||||
"js/item_lifecycle_decay_system.js": "global.TarinaiItemDecaySystem",
|
||||
"js/item_lifecycle_growth_system.js": "global.TarinaiItemGrowthSystem",
|
||||
"js/item_lifecycle_step_frame.js": "global.TarinaiItemFrameLifecycleStep",
|
||||
"js/item_lifecycle_step_decay.js": "global.TarinaiItemDecayLifecycleStep",
|
||||
"js/item_lifecycle_step_dynamic.js": "global.TarinaiItemDynamicLifecycleStep",
|
||||
"js/item_lifecycle_step_growth.js": "global.TarinaiItemGrowthLifecycleStep",
|
||||
"js/item_lifecycle_step_legacy.js": "global.TarinaiItemLegacyLifecycleStep",
|
||||
"js/item_lifecycle_pipeline.js": "global.TarinaiItemLifecyclePipeline",
|
||||
}
|
||||
for rel, token in exports.items():
|
||||
if token not in read_text(ROOT / rel):
|
||||
fail(f"Item lifecycle pipeline export missing: {rel}")
|
||||
runtime = read_text(ROOT / "js/item_runtime.js")
|
||||
if "TarinaiItemLifecyclePipeline.updateOne" not in runtime or "legacyUpdate: fn" not in runtime:
|
||||
fail("item runtime does not route through TarinaiItemLifecyclePipeline with legacy fallback")
|
||||
pipeline = read_text(ROOT / "js/item_lifecycle_pipeline.js")
|
||||
for token in ["TarinaiItemFrameLifecycleStep", "TarinaiItemDecayLifecycleStep", "TarinaiItemDynamicLifecycleStep", "TarinaiItemGrowthLifecycleStep", "TarinaiItemLegacyLifecycleStep"]:
|
||||
if token not in pipeline:
|
||||
fail(f"Item lifecycle pipeline missing step token: {token}")
|
||||
dynamic_step = read_text(ROOT / "js/item_lifecycle_step_dynamic.js")
|
||||
dynamic_system = read_text(ROOT / "js/item_dynamic_system.js")
|
||||
if "TarinaiItemDynamicSystem?.update" not in dynamic_step:
|
||||
fail("Item dynamic lifecycle step does not delegate to TarinaiItemDynamicSystem")
|
||||
for token in ["TarinaiItemDynamicToolSystem", "TarinaiItemDynamicBallSystem", "TarinaiItemDynamicDuplicatorSystem", "TarinaiItemDynamicPinSystem", "TarinaiItemDynamicZunchiSystem"]:
|
||||
if token not in dynamic_system:
|
||||
fail(f"Item dynamic system missing subsystem token: {token}")
|
||||
for rel in ["js/item_dynamic_tool_system.js", "js/item_dynamic_ball_system.js", "js/item_dynamic_duplicator_system.js", "js/item_dynamic_pin_system.js", "js/item_dynamic_zunchi_system.js"]:
|
||||
body = read_text(ROOT / rel)
|
||||
for token in ["item.updateFan", "item.updateMagnet", "item.updateRotator", "item.updateBall", "item.updateDuplicator", "item.updatePushpin", "item.updateZunchiMotion", "item.updateZunchi"]:
|
||||
if token in body:
|
||||
fail(f"dynamic subsystem still delegates to prototype method {token} in {rel}")
|
||||
lifecycle_runtime = read_text(ROOT / "js/item_lifecycle_runtime.js")
|
||||
lifecycle_support = read_text(ROOT / "js/item_lifecycle_support.js")
|
||||
lifecycle_decay = read_text(ROOT / "js/item_lifecycle_step_decay.js")
|
||||
lifecycle_growth = read_text(ROOT / "js/item_lifecycle_step_growth.js")
|
||||
if "TarinaiItemLifecycleRuntimeSupport" not in lifecycle_support or "TarinaiDuplicatorRuntime" not in lifecycle_support:
|
||||
fail("item lifecycle support export missing")
|
||||
if "TarinaiItemDecaySystem?.update" not in lifecycle_decay or "TarinaiItemGrowthSystem?.update" not in lifecycle_growth:
|
||||
fail("item lifecycle decay/growth steps are not thin system adapters")
|
||||
for helper_token in ["function fanAffectsPoint", "function duplicatorLoadTypeForItem", "function duplicatorApplyStoredPin"]:
|
||||
if helper_token in lifecycle_runtime:
|
||||
fail(f"support helper still lives in item_lifecycle_runtime.js: {helper_token}")
|
||||
for token in ["updateFan(dt, worldRef) { return window.TarinaiItemDynamicToolSystem", "updateBall(dt, worldRef) { return window.TarinaiItemDynamicBallSystem", "attachPushpin(t, worldRef"]:
|
||||
if token not in lifecycle_runtime:
|
||||
fail(f"prototype compatibility facade missing: {token}")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
context.isServingFoodType = (type) => type === 'food';
|
||||
context.passiveFoodDecayInterval = () => 0.5;
|
||||
context.passiveFoodDecayRate = () => 0.1;
|
||||
context.PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING = 0.055;
|
||||
context.TarinaiItemRegistry = {{ food: {{ hygienePenalty() {{ return 0.05; }} }} }};
|
||||
context.isPinType = (type) => type === 'pushpin';
|
||||
context.normalizeGrassStage = (item) => {{ item.normalized = true; }};
|
||||
context.stableUnit = () => 0.5;
|
||||
vm.createContext(context);
|
||||
for (const file of {required!r}) {{
|
||||
vm.runInContext(fs.readFileSync(file, 'utf8'), context, {{ filename: file }});
|
||||
}}
|
||||
if (!context.TarinaiItemLifecyclePipeline.canRunConcretePipeline()) throw new Error('item pipeline cannot run concrete steps');
|
||||
const events = [];
|
||||
const world = {{
|
||||
itemDropImpact(item) {{ events.push(['drop', item.type]); }},
|
||||
registerFoodSpoilage(value, item) {{ events.push(['spoil', value, item.type]); }},
|
||||
markTerrainDirty(reason) {{ events.push(['terrain', reason]); }},
|
||||
emit(type, payload) {{ events.push(['emit', type, payload.type]); }},
|
||||
}};
|
||||
const food = {{ type: 'food', dead: false, age: 0, dropTimer: 0.1, dropImpactDone: false, foodServingsRemaining: 1, amount: 1, passiveFoodDecayTimer: 0 }};
|
||||
if (!context.TarinaiItemLifecyclePipeline.updateOne(food, 0.6, world, {{ legacyUpdate() {{ throw new Error('legacy should not run'); }} }})) throw new Error('food update failed');
|
||||
if (food.age !== 0.6 || !food.dropImpactDone || !(food.foodServingsRemaining < 1) || !events.some(([kind]) => kind === 'spoil')) throw new Error('frame/decay step did not run');
|
||||
context.CONFIG = {{ worldPadding: 30 }};
|
||||
context.clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||
context.distXY = (ax, ay, bx, by) => Math.hypot(ax - bx, ay - by);
|
||||
const dynamic = {{
|
||||
type: 'ball', dead: false, age: 0, x: 80, y: 80, prevX: 80, prevY: 80,
|
||||
vx: 12, vy: 0, r: 18, spin: 0, spinVelocity: 0
|
||||
}};
|
||||
const ballWorld = {{ ...world, w: 500, h: 500, nearbyItems() {{ return []; }} }};
|
||||
context.TarinaiItemLifecyclePipeline.updateOne(dynamic, 0.25, ballWorld, {{ legacyUpdate() {{ throw new Error('legacy should not run'); }} }});
|
||||
if (!(dynamic.x > 80) || !(dynamic.spin > 0)) throw new Error('dynamic step did not run');
|
||||
const grass = {{ type: 'grass', dead: false, age: 0 }};
|
||||
context.TarinaiItemLifecyclePipeline.updateOne(grass, 0.1, world, {{ legacyUpdate() {{ throw new Error('legacy should not run'); }} }});
|
||||
if (!grass.normalized) throw new Error('growth step did not run');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("Item lifecycle pipeline smoke test failed")
|
||||
ok("Item lifecycle pipeline boundary present")
|
||||
|
||||
def main() -> None:
|
||||
data = manifest()
|
||||
check_files_exist(data)
|
||||
|
|
@ -610,7 +1239,14 @@ def main() -> None:
|
|||
check_item_bucket_polish()
|
||||
check_current_save_format()
|
||||
check_phase_and_input_split()
|
||||
check_system_order_boundary()
|
||||
simulation_boundary_smoke()
|
||||
check_entity_runtime_boundary()
|
||||
check_item_lifecycle_pipeline_boundary()
|
||||
check_tarinai_update_pipeline_boundary()
|
||||
check_update_policy_boundaries()
|
||||
check_processing_split()
|
||||
command_dispatcher_smoke()
|
||||
check_legacy_behavior_refs()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue