aaa
This commit is contained in:
parent
f1c3af2b89
commit
2636d580a8
75 changed files with 3890 additions and 1084 deletions
|
|
@ -2,8 +2,7 @@
|
|||
"""Lightweight regression guard for the buildless Tarinai app.
|
||||
|
||||
Checks static-file consistency, generated-load order, syntax, and the ActionSpec
|
||||
registry contract. This intentionally avoids save-data migration checks; save
|
||||
compatibility is intentionally not retained for save format changes.
|
||||
registry contract. This covers static contracts and focused runtime smoke tests. The current codec intentionally supports only the current binary schema.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -250,6 +249,26 @@ def check_legacy_physics_props_removed() -> None:
|
|||
ok("legacy physics property names removed")
|
||||
|
||||
|
||||
def check_redundant_correction_paths_removed() -> None:
|
||||
terms = [
|
||||
"markBodyChanged", "purgeLegacyPhysicsStorage", "nextTarinaiCollisionCheckAt",
|
||||
"_physicsWorldFrame", "bodyReads", "bodyWrites", "constraintReads", "constraintWrites",
|
||||
"compactEffects",
|
||||
]
|
||||
offenders = []
|
||||
for path in (ROOT / "js").glob("*.js"):
|
||||
text = read_text(path)
|
||||
for term in terms:
|
||||
if term in text:
|
||||
offenders.append(f"{path.relative_to(ROOT).as_posix()}:{term}")
|
||||
if offenders:
|
||||
fail("redundant correction path returned: " + ", ".join(offenders[:20]))
|
||||
ball_source = read_text(ROOT / "js/item_dynamic_ball_system.js")
|
||||
if ball_source.count("tryBallPickup(") != 1:
|
||||
fail("ball update must perform exactly one pickup check")
|
||||
ok("redundant correction paths remain removed")
|
||||
|
||||
|
||||
def check_basic_action_spec_runtime() -> None:
|
||||
source = "\n".join(read_text(ROOT / rel) for rel in ["js/tarinai_action_definitions.js", "js/tarinai_needs_items.js"])
|
||||
required = {
|
||||
|
|
@ -459,20 +478,47 @@ def check_current_save_format() -> None:
|
|||
fail("broad snapshot fields still present after light-save conversion: " + ", ".join(offenders))
|
||||
required_codec = [
|
||||
'EXPORT_PREFIX = "\\u305f"',
|
||||
'SAVE_TEXT_BITS = 12',
|
||||
'SAVE_HEADER_CHARS = "\\u3089\\u308A\\u308B"',
|
||||
'SAVE_TEXT_BITS = 15',
|
||||
"writeStringTable",
|
||||
"SAVE_TEXT_ALPHABET",
|
||||
"jp4096Encode",
|
||||
"jp4096Decode",
|
||||
"writeCurrentItemExtra",
|
||||
"readCurrentItemExtra",
|
||||
"saveTextCharFromValue",
|
||||
"saveTextValueFromCode",
|
||||
"base32768Encode",
|
||||
"base32768Decode",
|
||||
"makeSaveFrame",
|
||||
"openSaveFrame",
|
||||
"writeTarinaiBitPlane",
|
||||
"readTarinaiBitPlane",
|
||||
"TARINAI_COLUMN_LAYOUT",
|
||||
"chooseCommonExtraPlan",
|
||||
"standardItemAmount",
|
||||
"chooseAmountPlan",
|
||||
"worldSeedWords",
|
||||
"saveTextCharFromValue",
|
||||
"saveTextValueFromCode",
|
||||
"itemFlagPlaneBytes",
|
||||
"writeNormalItemExtra",
|
||||
"readNormalItemExtra",
|
||||
"writeBirthDescriptor",
|
||||
"readBirthDescriptor",
|
||||
"async function encodeSnapshot",
|
||||
"async function decodeSnapshot",
|
||||
]
|
||||
for token in required_codec:
|
||||
if token not in save_codec:
|
||||
fail(f"binary save codec token missing: {token}")
|
||||
if "SAVE_TEXT_ALPHABET" in save_codec or "SAVE_TEXT_REVERSE" in save_codec:
|
||||
fail("15-bit save text still allocates the legacy alphabet or reverse map")
|
||||
if "writer.u(Snapshot.version)" in save_codec:
|
||||
fail("duplicate snapshot version remains in the binary header")
|
||||
if "writeSaveString(writer, w[8]" in save_codec:
|
||||
fail("worldSeed is still stored as a string")
|
||||
for token in ("formatHeader & 0x7f", "formatHeader & 0x80", "payload.length * 2 + (useCompressed ? 1 : 0)"):
|
||||
if token not in save_codec:
|
||||
fail(f"compact save framing token missing: {token}")
|
||||
if "tailMask" in save_codec:
|
||||
fail("redundant Tarinai tailMask remains in current schema")
|
||||
if "costBits < best.costBits" not in save_codec or "itemFlagPlaneBytes" not in save_codec:
|
||||
fail("cost-based common extra or continuous item flag plane is missing")
|
||||
required_storage = [
|
||||
'STORAGE_PREFIX = "tarinai_japanese_hash_slot_v1_"',
|
||||
'hash.startsWith("\\u305f")',
|
||||
|
|
@ -1277,10 +1323,14 @@ context.window = context;
|
|||
context.TarinaiPerf = {{ begin() {{ return null; }} }};
|
||||
context.TarinaiConstraintSystem = {{ updateWorld() {{ return 0; }} }};
|
||||
context.normalizedItemAngle = angle => angle;
|
||||
context.isRotatableItemType = type => ['rotator', 'poison_block', 'reciprocator'].includes(String(type || ''));
|
||||
context.defaultItemAngle = type => String(type || '') === 'reciprocator' ? Math.PI / 2 : 0;
|
||||
context.itemRadiusFor = (_type, fallback = 12) => fallback;
|
||||
context.toolItemType = tool => tool;
|
||||
vm.createContext(context);
|
||||
for (const path of {repr([str(ROOT / rel) for rel in [
|
||||
'js/math.js', 'js/geometry_helpers.js', 'js/collision_footprint_system.js',
|
||||
'js/physics_world_system.js', 'js/mechanical_system.js', 'js/physics_shape_editor_system.js'
|
||||
'js/physics_world_system.js', 'js/mechanical_system.js', 'js/placement_preview_system.js', 'js/physics_shape_editor_system.js'
|
||||
]])}) vm.runInContext(fs.readFileSync(path, 'utf8'), context, {{ filename: path }});
|
||||
|
||||
const fp = context.TarinaiCollisionFootprints;
|
||||
|
|
@ -1308,6 +1358,16 @@ function activate(item) {{
|
|||
if (item.type === 'reciprocator') {{ api.setScalar(item, 'railOn', true, 'test'); api.setScalar(item, 'railAxis', 0, 'test'); api.setScalar(item, 'railMotorSpeed', 80, 'test'); }}
|
||||
if (item.type === 'poison_block') {{ api.setScalar(item, 'xv', 24, 'test'); api.setScalar(item, 'awakeUntil', 10, 'test'); }}
|
||||
}}
|
||||
const reverseRotator = makeItem('rotator', 80, 80);
|
||||
reverseRotator.world = {{ time: 0 }};
|
||||
context.TarinaiPhysicsBodySystem.ensureBody(reverseRotator, reverseRotator.world);
|
||||
context.TarinaiPhysicsBodySystem.setScalar(reverseRotator, 'motorSpeed', -1.25, 'reverse-test');
|
||||
if (Math.abs(context.TarinaiPhysicsBodySystem.scalar(reverseRotator, 'motorSpeed', 0) + 1.25) > 1e-9) throw new Error('negative rotator motor speed was clamped');
|
||||
reverseRotator.angle = 1.0;
|
||||
reverseRotator.world = {{ time: 0, drawListDirty: false, markSpatialDirty() {{}}, nearbyItems() {{ return []; }}, nearbyObstacles() {{ return []; }}, items: [], tarinai: [] }};
|
||||
context.TarinaiMechanicalSystem.updateRotator(reverseRotator, 0.1, reverseRotator.world, {{ skipInteractions: true }});
|
||||
if (!(reverseRotator.angle < 0.99)) throw new Error('negative rotator speed did not rotate backward');
|
||||
|
||||
function pairResult(typeA, typeB, disableB = false) {{
|
||||
const a = makeItem(typeA, 120, 120);
|
||||
const b = makeItem(typeB, 120, 128);
|
||||
|
|
@ -1330,6 +1390,67 @@ for (const [a, b] of [['rotator', 'rotator'], ['rotator', 'reciprocator'], ['rot
|
|||
const disabled = pairResult(a, b, true);
|
||||
if (disabled.solved !== 0) throw new Error(`${{a}}/${{b}} collision-off pair still interacted`);
|
||||
}}
|
||||
|
||||
const orientationWorld = {{ toolAngleFor() {{ return 0; }} }};
|
||||
const previewReciprocator = makeItem('reciprocator', 40, 40);
|
||||
context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, previewReciprocator, 0);
|
||||
if (Math.abs(previewReciprocator.physicsBody.pose.angle - Math.PI / 2) > 1e-9) throw new Error('reciprocator placement body angle mismatch');
|
||||
if (Math.abs(previewReciprocator.physicsBody.rail.axisAngle) > 1e-9) throw new Error('reciprocator placement rail angle mismatch');
|
||||
|
||||
const rail = makeItem('reciprocator', 0, 0);
|
||||
context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, rail, 0);
|
||||
activate(rail);
|
||||
const fence = {{ id: 'fence', type: 'fence_v', x: 8, y: 0, amount: 999, dead: false }};
|
||||
const collisionWorld = {{
|
||||
items: [rail, fence], tarinai: [], time: 1, frameCount: 1, w: 900, h: 700, drawListDirty: false, itemBucketRebuildsTotal: 0,
|
||||
ensureItemBuckets() {{}}, markSpatialDirty() {{}},
|
||||
nearbyObstacles() {{ return this.items; }}, nearbyItems() {{ return this.items; }},
|
||||
itemsOfType(type) {{ return this.items.filter(item => item.type === type && !item.dead); }},
|
||||
isFenceType(type) {{ return type === 'fence_v'; }},
|
||||
solidObstacleRects(item) {{
|
||||
if (item !== fence) return [];
|
||||
return [{{ left: 4, right: 12, top: -100, bottom: 100, cx: 8, cy: 0, halfW: 4, halfH: 100, angle: 0, cos: 1, sin: 0, oriented: true, type: 'fence_v', item }}];
|
||||
}},
|
||||
}};
|
||||
rail.world = collisionWorld; fence.world = collisionWorld;
|
||||
context.TarinaiMechanicalSystem.updateWorld(collisionWorld, 0.016, {{ maxSubsteps: 1 }});
|
||||
if (context.TarinaiPhysicsBodySystem.scalar(rail, 'railDir', 1) !== -1) throw new Error('reciprocator did not reverse at a solid obstacle');
|
||||
|
||||
const oneWayRail = makeItem('reciprocator', 0, 0);
|
||||
context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, oneWayRail, 0);
|
||||
activate(oneWayRail);
|
||||
const oneWayFence = {{ id: 'one-way-fence', type: 'one_way_fence', x: 8, y: 0, amount: 999, dead: false }};
|
||||
const oneWayWorld = {{
|
||||
items: [oneWayRail, oneWayFence], tarinai: [], time: 1, frameCount: 2, w: 900, h: 700, drawListDirty: false, itemBucketRebuildsTotal: 0,
|
||||
ensureItemBuckets() {{}}, markSpatialDirty() {{}},
|
||||
nearbyObstacles() {{ return this.items; }}, nearbyItems() {{ return this.items; }},
|
||||
itemsOfType(type) {{ return this.items.filter(item => item.type === type && !item.dead); }},
|
||||
isFenceType(type) {{ return type === 'one_way_fence'; }},
|
||||
solidObstacleRects(item) {{
|
||||
if (item !== oneWayFence) return [];
|
||||
return [{{ left: 4, right: 12, top: -100, bottom: 100, cx: 8, cy: 0, halfW: 4, halfH: 100, angle: 0, cos: 1, sin: 0, oriented: true, type: 'one_way_fence', item, oneWay: true, oneWayNx: 1, oneWayNy: 0 }}];
|
||||
}},
|
||||
}};
|
||||
oneWayRail.world = oneWayWorld; oneWayFence.world = oneWayWorld;
|
||||
context.TarinaiMechanicalSystem.updateWorld(oneWayWorld, 0.016, {{ maxSubsteps: 1 }});
|
||||
if (!Number.isFinite(context.TarinaiPhysicsBodySystem.scalar(oneWayRail, 'railDir', 0))) throw new Error('one-way mechanical fence update produced an invalid rail direction');
|
||||
|
||||
|
||||
const cargoRail = makeItem('reciprocator', 0, 0);
|
||||
context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, cargoRail, 0);
|
||||
activate(cargoRail);
|
||||
const ball = {{ id: 'ball', type: 'ball', x: 8, y: 0, r: 18, radius: 18, vx: 0, vy: 0, dead: false }};
|
||||
const cargoWorld = {{
|
||||
items: [cargoRail, ball], tarinai: [], time: 2, frameCount: 2, w: 900, h: 700, drawListDirty: false, itemBucketRebuildsTotal: 0,
|
||||
ensureItemBuckets() {{}}, markSpatialDirty() {{}},
|
||||
nearbyObstacles() {{ return this.items; }}, nearbyItems() {{ return this.items; }},
|
||||
itemsOfType(type) {{ return this.items.filter(item => item.type === type && !item.dead); }},
|
||||
isFenceType() {{ return false; }}, solidObstacleRects() {{ return []; }},
|
||||
}};
|
||||
cargoRail.world = cargoWorld; ball.world = cargoWorld;
|
||||
context.TarinaiMechanicalSystem.updateWorld(cargoWorld, 0.016, {{ maxSubsteps: 1 }});
|
||||
if (context.TarinaiPhysicsBodySystem.scalar(cargoRail, 'railDir', 1) !== 1) throw new Error('reciprocator reversed on movable cargo');
|
||||
if (!(ball.x > 8 || Math.abs(ball.vx) > 0.01 || Math.abs(ball.vy) > 0.01)) throw new Error('reciprocator did not push movable cargo');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
|
|
@ -1342,7 +1463,7 @@ for (const [a, b] of [['rotator', 'rotator'], ['rotator', 'reciprocator'], ['rot
|
|||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("mechanical collision/editor smoke test failed")
|
||||
ok("collision-enabled mechanical pairs interact and editor geometry snaps to the 20px lattice")
|
||||
ok("mechanical pairs, obstacle-only reciprocator reversal, cargo pushing, placement orientation, and 20px editor snapping verified")
|
||||
|
||||
def colony_limits_and_defaults_smoke() -> None:
|
||||
tool_defs = read_text(ROOT / "js/item_tool_definitions.js")
|
||||
|
|
@ -1403,8 +1524,17 @@ def colony_limits_and_defaults_smoke() -> None:
|
|||
for rel, token in guarded_sources.items():
|
||||
if token not in read_text(ROOT / rel):
|
||||
fail(f"object-limit guard missing from {rel}: {token}")
|
||||
if "championMate" not in needs or "championPair" not in social or "mateWeight *= 5.0" not in social:
|
||||
if "championMate" not in needs or "championPair" not in social or "mateWeight *= 4.0" not in social:
|
||||
fail("champion-pair breeding preference is incomplete")
|
||||
charts = read_text(ROOT / "js/ui_charts.js")
|
||||
family_social = read_text(ROOT / "js/world_family_social.js")
|
||||
if 'key: "births"' not in charts or 'key: "deaths"' not in charts or "birthEventCount" not in charts:
|
||||
fail("colony population chart birth/death series are missing")
|
||||
if "inheritedChampionPersonality" not in family_social or "parent.isTarinaiChampion" not in family_social or "* 0.70" not in family_social:
|
||||
fail("champion personality inheritance is missing")
|
||||
inheritance_block = family_social.split("function inheritedChampionPersonality", 1)[1].split("(function (global)", 1)[0]
|
||||
if "isZunchiSlave" in inheritance_block or "zunchiSlaveLocked" in inheritance_block:
|
||||
fail("champion inheritance must not alter zunchi-slave handling")
|
||||
for token in ("tarinaiPopulationLimit", "objectLimit"):
|
||||
if token not in snapshot or token not in codec:
|
||||
fail(f"colony limit persistence missing: {token}")
|
||||
|
|
@ -1457,14 +1587,14 @@ world.tarinai[0].dead = true;
|
|||
if (!world.canAddTarinai(1)) throw new Error('dead tarinai should free capacity');
|
||||
world.setObjectLimit('');
|
||||
if (world.objectLimit !== 0 || !world.canAddObjects(1000)) throw new Error('blank/zero must mean unlimited');
|
||||
context.TarinaiSnapshot = {{ version: 23 }};
|
||||
context.TarinaiSaveSchema = {{ BINARY_SCHEMA_VERSION: 28, FIELD_IDS: ['garden'] }};
|
||||
context.TarinaiSnapshot = {{ version: 27 }};
|
||||
context.TarinaiSaveSchema = {{ BINARY_SCHEMA_VERSION: 33, FIELD_IDS: ['garden'], itemTypeValue(i, fallback = '') {{ return fallback; }} }};
|
||||
context.CONFIG.dayLength = 120;
|
||||
context.TextEncoder = TextEncoder;
|
||||
context.TextDecoder = TextDecoder;
|
||||
vm.runInContext(fs.readFileSync({str(ROOT / 'js/save_codec.js')!r}, 'utf8'), context);
|
||||
(async () => {{
|
||||
const source = {{ v: 23, a: 'tj1', m: [1, 0, 'garden'], w: [0, 0, 0, 120, 0, -9990, 1, 0, 'seed', 0, 12, 345], t: [], i: [] }};
|
||||
const source = {{ v: 27, a: 'tj1', m: [1, 0, 'garden'], w: [0, 0, 0, 120, 0, -9990, 1, 0, 'seed', 0, 12, 345], t: [], i: [] }};
|
||||
const encoded = await context.TarinaiSaveCodec.encodeSnapshot(source);
|
||||
const decoded = await context.TarinaiSaveCodec.decodeSnapshot(encoded);
|
||||
if (decoded.w[10] !== 12 || decoded.w[11] !== 345) throw new Error('colony limits did not survive save roundtrip');
|
||||
|
|
@ -1492,7 +1622,7 @@ def physics_tuning_smoke() -> None:
|
|||
dynamic_tools = read_text(ROOT / "js/item_dynamic_tool_system.js")
|
||||
initializer = read_text(ROOT / "js/item_type_initializers.js")
|
||||
|
||||
for token in ("BOUNCE_FENCE_RESTITUTION = 2.40", "BOUNCE_FENCE_MIN_SPEED = 520", "BOUNCE_FENCE_MAX_SPEED = 1180"):
|
||||
for token in ("BOUNCE_FENCE_RESTITUTION = 4.20", "BOUNCE_FENCE_MIN_SPEED = 820", "BOUNCE_FENCE_MAX_SPEED = 1680"):
|
||||
if token not in environment:
|
||||
fail(f"strong bounce-fence tuning missing: {token}")
|
||||
if "const iterations = 8" not in constraints or "const compliance = mode === \"rod\" ? 0.085 / (step * 60) : 0" not in constraints:
|
||||
|
|
@ -1573,10 +1703,7 @@ context.TarinaiPhysicsBodySystem = {{
|
|||
setParticles(item, list) {{ item.physicsConstraint.particles = list; return true; }},
|
||||
ensureConstraint(item) {{ return item.physicsConstraint; }},
|
||||
applyConstraintState() {{ return true; }},
|
||||
normalizeConstraintState(_item, state) {{ return state; }},
|
||||
syncPoseFromItem() {{ return true; }},
|
||||
normalizeBodyState() {{ return true; }},
|
||||
markBodyChanged() {{ return true; }},
|
||||
}};
|
||||
vm.runInContext(constraintSource, context, {{ filename: 'constraint_system.js' }});
|
||||
const a = {{ id: 'a', type: 'ball', x: 0, y: 0, r: 10, dead: false }};
|
||||
|
|
@ -1629,6 +1756,105 @@ if (Math.abs(capturedScale - 0.68) > 1e-9) throw new Error('sticky bomb does not
|
|||
fail("physics tuning smoke test failed")
|
||||
ok("round circle template, strong bounce fence, inextensible rope, and small sticky-bomb blast verified")
|
||||
|
||||
|
||||
|
||||
|
||||
def champion_inheritance_smoke() -> None:
|
||||
source = read_text(ROOT / "js/world_family_social.js")
|
||||
start = source.find("function inheritedChampionPersonality")
|
||||
end = source.find("(function (global)", start)
|
||||
if start < 0 or end < 0:
|
||||
fail("champion inheritance helper not found")
|
||||
helper = source[start:end]
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.normalizePersonality = source => ({{
|
||||
aggression: Math.max(-1, Math.min(1, Number(source?.aggression || 0))),
|
||||
openness: Math.max(-1, Math.min(1, Number(source?.openness || 0))),
|
||||
sociability: Math.max(-1, Math.min(1, Number(source?.sociability || 0))),
|
||||
neuroticism: Math.max(-1, Math.min(1, Number(source?.neuroticism || 0))),
|
||||
}});
|
||||
context.clampPersonalityValue = value => Math.max(-1, Math.min(1, Number(value) || 0));
|
||||
context.TarinaiSeedFactory = {{ birthProfile() {{ return {{ birthPersonality: {{ aggression: 0, openness: 0, sociability: 0, neuroticism: 0 }} }}; }} }};
|
||||
vm.createContext(context);
|
||||
vm.runInContext({helper!r}, context, {{ filename: 'champion_inheritance.js' }});
|
||||
const normal = {{ birthSeed: 'normal', birthPersonality: {{ aggression: 0, openness: 0, sociability: 0, neuroticism: 0 }}, isTarinaiChampion: false }};
|
||||
const champion = {{ birthSeed: 'champion', birthPersonality: {{ aggression: 0, openness: 0, sociability: 0, neuroticism: 0 }}, isTarinaiChampion: true }};
|
||||
const child = context.inheritedChampionPersonality('child', [champion, normal], {{}});
|
||||
if (!child || child.aggression < 0.5 || child.openness < 0.5 || child.sociability < 0.5) throw new Error('champion boost was not inherited');
|
||||
if (Math.abs(child.neuroticism) > 1e-9) throw new Error('unboosted personality axis was inherited');
|
||||
const descendant = {{ birthSeed: 'descendant', birthPersonality: child, isTarinaiChampion: false }};
|
||||
const grandchild = context.inheritedChampionPersonality('grandchild', [descendant, normal], {{}});
|
||||
if (!grandchild || !(grandchild.aggression > 0 && grandchild.aggression < child.aggression)) throw new Error('champion lineage did not damp across generations');
|
||||
"""
|
||||
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("champion inheritance smoke test failed")
|
||||
ok("champion personality inheritance and generational damping verified")
|
||||
|
||||
def one_way_routing_smoke() -> None:
|
||||
geometry_source = read_text(ROOT / "js/geometry_helpers.js")
|
||||
source = read_text(ROOT / "js/world_environment.js")
|
||||
render = read_text(ROOT / "js/render.js")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
class World {{}}
|
||||
const context = {{ console, World, globalThis: null, window: null, CONFIG: {{}},
|
||||
TarinaiMechanicalShapeSystem: {{ isMechanicalType() {{ return false; }}, reach() {{ return 0; }} }},
|
||||
TarinaiMechanicalSystem: {{}}, TarinaiPhysicsBodySystem: {{}},
|
||||
}};
|
||||
context.globalThis = context; context.window = context;
|
||||
vm.createContext(context);
|
||||
vm.runInContext({geometry_source!r}, context, {{ filename: 'geometry_helpers.js' }});
|
||||
vm.runInContext({source!r}, context, {{ filename: 'world_environment.js' }});
|
||||
const world = new context.World();
|
||||
world.isFenceType = type => ['fence_h', 'one_way_fence'].includes(type);
|
||||
const oneWay = {{ type: 'one_way_fence', item: {{ type: 'one_way_fence' }}, oneWay: true, oneWayNx: 1, oneWayNy: 0, oriented: false, left: -4, right: 4, top: -20, bottom: 20 }};
|
||||
world.nearbySolidObstacleRects = () => [oneWay];
|
||||
if (world.pathBlockedByFence(-30, 0, 30, 0, 0)) throw new Error('one-way fence blocked arrow direction');
|
||||
if (!world.pathBlockedByFence(30, 0, -30, 0, 0)) throw new Error('one-way fence allowed reverse direction');
|
||||
if (world.pointBlockedByObstacle(0, 0, 0, {{ directionalOneWay: true }})) throw new Error('one-way fence permanently blocked grid point');
|
||||
if (!world.pointBlockedByObstacle(0, 0, 0)) throw new Error('one-way fence lost physical point occupancy');
|
||||
const normal = {{ type: 'fence_h', item: {{ type: 'fence_h' }}, oriented: false, left: -4, right: 4, top: -20, bottom: 20 }};
|
||||
world.nearbySolidObstacleRects = () => [normal];
|
||||
if (!world.pathBlockedByFence(-30, 0, 30, 0, 0)) throw new Error('ordinary fence stopped blocking routes');
|
||||
"""
|
||||
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("one-way fence routing smoke test failed")
|
||||
if 'type === "one_way_fence"' not in render or "arrowLength" not in render:
|
||||
fail("one-way fence placement direction overlay is missing")
|
||||
ok("one-way fence preview and directional routing verified")
|
||||
|
||||
def check_save_stream_deadlock_guard() -> None:
|
||||
codec = read_text(ROOT / "js/save_codec.js")
|
||||
ui = read_text(ROOT / "js/save_system.js")
|
||||
read_task = codec.find("const readTask = (async () =>")
|
||||
write_call = codec.find("await sink.write(")
|
||||
if read_task < 0 or write_call < 0 or read_task > write_call:
|
||||
fail("save compression output must be drained before awaiting writes")
|
||||
if 'exportBtn.dataset.busy = "1"' not in ui or 'save text generation failed' not in ui:
|
||||
fail("save export UI must recover from pending/error states")
|
||||
ok("save compression stream backpressure guard present")
|
||||
|
||||
def main() -> None:
|
||||
data = manifest()
|
||||
check_files_exist(data)
|
||||
|
|
@ -1643,6 +1869,8 @@ def main() -> None:
|
|||
need_action_runtime_bridge_smoke()
|
||||
mechanical_collision_and_editor_smoke()
|
||||
physics_tuning_smoke()
|
||||
champion_inheritance_smoke()
|
||||
one_way_routing_smoke()
|
||||
colony_limits_and_defaults_smoke()
|
||||
check_basic_action_spec_runtime()
|
||||
check_social_action_spec_runtime()
|
||||
|
|
@ -1651,6 +1879,7 @@ def main() -> None:
|
|||
check_social_balance_guards()
|
||||
check_item_bucket_polish()
|
||||
check_current_save_format()
|
||||
check_save_stream_deadlock_guard()
|
||||
check_phase_and_input_split()
|
||||
check_system_order_boundary()
|
||||
simulation_boundary_smoke()
|
||||
|
|
@ -1662,6 +1891,7 @@ def main() -> None:
|
|||
command_dispatcher_smoke()
|
||||
check_legacy_behavior_refs()
|
||||
check_legacy_physics_props_removed()
|
||||
check_redundant_correction_paths_removed()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue