Experiment 20260925T110041 · Codex · xhigh

Proc-8:从八份建筑参考
到共享生成与验证库


Hearthwright:用轻量 Python 描述体量、楼层、连接、屋顶和场地,让同一份 library 完成规划、建造、验证与导出。

这次输入来自 runs/exp8 的八种建筑。目标是可组合的建筑语言:新房子由新客户端表达,用户控制 seed,库承担几何与约束。

由三个相连体量组成的新庭院住宅
新组合:三翼庭院,seed 0。三个体量共用同一套库。
13轻量客户端
225通过的测试
832审计生成次数
26保存的样本
84 minagent 运行时间
报告目录

01 · 关键观察

交付了可组合的共享库。13 个库模块共 2,535 行 Python,13 个示例客户端的目录共 344 行(包含空初始化文件)。Design / Volume / Level / Join / Roof / Site 表达建筑意图;代码中没有按八种建筑名称分派的 library API。名称选择仅发生在用户选择运行哪个示例文件时。

结构覆盖比单一 lodge 更广。浅坡瓦屋顶、树根外壳、水上桩基、内缩底层与悬挑楼层、三翼庭院均有实际样本。八个参考客户端之外,还有运河药剂师住宅、林间书库、果园住宅、山坡工作室、三翼庭院五个新组合。

seed 不等于建筑种类。每个客户端测试 64 个 seed,实际观察到 8–45 个几何签名。26 个保存样本的签名均不同,只说明这批样本没有按该口径重复。没有估算整个组合空间。

浏览器迁移暴露了确定性边界。26 份样本结构签名全部一致,但 17 份吊灯位置随 Python 平台变化。报告保留原程序,并在第 12 节给出逐格比较证据。

测试通过说明实现通过了已有规则与有限样本检查;审美、对参考分布的覆盖程度,以及所有新组合是否成立,仍需分别判断。下方保留参考图、所有样本与剖切,便于直接比较。

02 · Live proc-gen:自己写一个建筑

点击“探索整个 distribution”,跨八份参考及五个新组合探索:以 13 份声明为先验,进一步采样体量尺寸、层数、侧翼增删与连接、屋顶、分区和场地。候选通过验证后才展示。这是有界、受约束的采样,不代表对所有可表达建筑的均匀或穷尽覆盖。

Python 在浏览器的 Web Worker 中运行。编辑下面的 DESIGN,或加载一个轻量客户端;再调整 seed 和可选参数,点击执行。首次运行需下载 Python 与 NumPy,后续复用运行时。生成成功后可旋转、缩放、剖切,并下载完整建筑。

完整 API 文档

示例的建筑声明来自原始客户端,载入时移除 CLI 入口,可直接改、增删体量。下方调用负责生成。示例代码的选择不限制 library 能表达的建筑。

运行一次后可选其它体量。空值保留代码中的固定值或 Choice / Span。

结构参数 · 可选覆盖

例如小体量可能容不下楼梯,hip 与 dormer 也有约束。失败时查看诊断,修改声明或清除覆盖。

探索会重新采样建筑方案与结构,成功后替换编辑器内容并更新参数控件;未勾选时也会随机换 seed。

已载入架空庄园客户端。点击执行,开始真实 Python 生成。

运行前展示的存档样本

存档预览 · 执行后替换为本次结果

40

ZIP 包含 building.litematic、验证所需的 building.json 和本次客户端。剖切只影响显示。容器物品写入 NBT;3D 预览不显示库存内容。

本次追加的 Python 调用 · 参数控件的实际作用
Python 输出 / 失败诊断
已生成建筑的解析参数、验证元数据与几何签名

03 · 任务与八份参考

Prompt:proc-8。输入为每种 exp8 建筑各一份最终生成代码和原始参考图,共 8 份代码、8 张图。与前次 proc-lodge 实验 的 ref-1–8 是不同输入集合。

地中海别墅 原始参考
地中海别墅 · 输入源码
商人庄园 原始参考
商人庄园 · 输入源码
红顶农庄 原始参考
红顶农庄 · 输入源码
树根住宅 原始参考
树根住宅 · 输入源码
砂岩小屋 原始参考
砂岩小屋 · 输入源码
水上高脚小屋 原始参考
水上高脚小屋 · 输入源码
架空庄园 原始参考
架空庄园 · 输入源码
石砌小屋 原始参考
石砌小屋 · 输入源码

任务要求共享生成与验证库、用户代码高层且轻量、用户控制 seed、注重结构布局多样性;库可以很大,程序长度不是优化目标。明确拒绝建筑家族选择器、参考名称分派与八个隐藏整屋预设。

16 份输入与任务快照逐字节比对一致。历史生成代码用于参考,不能当作正确性标准。参考来源与哈希。

完整英文 Prompt
# Task: Shared Procedural Architecture and Validation Library Build a reusable Python library for generating and validating furnished Minecraft Java 1.21.1 buildings and their immediate sites. Users must be able to write short, high-level client programs that compose the library's abstractions to generate a wide variety of complete, attractive, logically sound buildings. The eight `exp8` references below establish the intended architectural range and quality bar. The main deliverable is the shared generation and validation library, together with lightweight example clients, generated samples and tests. The library may be large and sophisticated. User code must remain small, readable and architectural: it describes what to build, while the library handles how to realize and verify it. Users explicitly control the seed. Do not introduce a building-family selector, a reference-name dispatch API, or an eight-preset generator. The references are examples from which to derive reusable abstractions, not an enumeration of supported building types. A new building should be expressible by a new composition of the public API, without adding a named building type or editing the library's core dispatch logic. Maximize the variety of buildings expressible through these compositions and their seeded parameter choices, while keeping generated samples within the reference distribution and satisfying the quality requirements below. One example invocation produces one dwelling or coherent property, such as a farmhouse with its farmyard. A thin `generate.py` client must preserve the harness contract: running it without arguments produces `output.litematic`. ## Eight references Exactly one final generator from each named `runs/exp8` run is supplied: | Reference | Source code | Original reference image | | --- | --- | --- | | mediterranean-villa | `references/mediterranean-villa/generate.py` | `reference_mediterranean_villa.jpg` | | merchant-manor | `references/merchant-manor/generate.py` | `reference_merchant_manor.jpg` | | red-roof-farmstead | `references/red-roof-farmstead/generate.py` | `reference_red_roof_farmstead.jpg` | | root-house | `references/root-house/generate.py` | `reference_root_house.jpg` | | sandstone-cottage | `references/sandstone-cottage/generate.py` | `reference_sandstone_cottage.jpg` | | stilt-water-cabin | `references/stilt-water-cabin/generate.py` | `reference_stilt_water_cabin.jpg` | | stilted-manor | `references/stilted-manor/generate.py` | `reference_stilted_manor.jpg` | | stone-cottage | `references/stone-cottage/generate.py` | `reference_stone_cottage.jpg` | `references/README.md` records provenance and SHA-256 hashes. These are the exp8 building references, not the ref-1 through ref-8 lodge set. Inspect all eight generators and images before choosing your procedural design. Images establish intended appearance; source code supplies implementation examples. Hidden sides and interiors must be complete. Text overlays, logos and presentation borders in the images are not architectural features to reproduce. This distribution contains several distinct architectural families, not one uniform lodge style: - **Mediterranean villa:** white plaster, blue or turquoise accents, tiled roofs at different heights, offset volumes, upper and lower terraces, open pergolas and a garden pool. The image includes furnished floor plans. Learn their circulation and domestic room relationships without freezing their exact layout. - **Merchant manor:** a stone undercroft, an elevated entrance terrace and exterior stair, timber and pale infill above, steep dark roofs, tower or spire accents, and a market stall with trading and storage spaces. - **Red-roof farmstead:** a timber farmhouse with a steep red roof and pale trim, dormer or cupola details, a furnished loft, and a composed working plot with a path, well shelter, fields and a small windmill. - **Root house:** an organic dwelling integrated into a hill and spreading dark timber roots. Vary the trunk and root silhouette, exposed stone, entrance recess and hollowed room layout. It must read as an inhabited root formation, not a conventional rectangular house with decorative logs attached. - **Sandstone cottage:** a broad pale roof with dark timber ridge and eaves, framed walls and a projecting entrance bay, a stair climbing the slope, a partly buried lower floor, and a waterside approach or jetty. - **Stilt water cabin:** a compact timber dwelling raised on posts over visible shallow water, a low overhanging gable, a railed deck, steps to a landing and a mooring, with a functional fishing cabin interior. - **Stilted manor:** a recessed stone ground floor, a genuinely overhanging timber upper floor carried by posts and braces, tall windows, a very steep roof, dormers and a cupola, with usable stacked interiors. - **Stone cottage:** a small, restrained stone-and-timber dwelling with a simple pale gable, recessed entry, framed windows and planters, a furnished main room and an optional usable loft. Preserve compact examples. Keep scale, proportions and detail comparable to the relevant references. A compact cottage should remain compact; a manor may be taller and more complex. Preserve the distinction between posts over water and posts supporting an overhanging manor, and between an earth-sheltered home and an ordinary masonry cellar. Use the supplied code as implementation reference. You may reuse and adapt useful geometry routines, but extract a coherent library of parameterized rules and components. All eight references must inform the library's capabilities. Demonstrate their architectural characteristics with lightweight compositions of the same public API, and demonstrate new compositions beyond those eight examples. Choose abstractions that separate architectural intent from geometric implementation. Possible concepts include sites and terrain relationships, connected volumes, levels, room programs, circulation constraints, envelopes, roof systems, support systems, attachments, material roles and furnishing policies. Users should combine such concepts rather than choose a complete building by name. Specialized geometry algorithms for roots, pitched roofs, earth sheltering or stilt supports are appropriate reusable capabilities. For example, a client could describe connected volumes on a slope, assign living and workshop spaces, request a pale pitched roof and a supported waterside deck, then generate and validate with a chosen seed. Another client could compose a recessed masonry base, an overhanging timber level and a steep roof with usable attic space. The library must resolve dimensions, joins, supports, openings, stairs, furnishing and clearance. These describe API intent, not required class names or a prescribed framework. Compositions must remain architecturally coherent. Do not require arbitrary feature mixing or put every motif in every sample. Preserve the references' different identities, proportions and scales through component rules and constraints, without encoding each whole reference as a hidden fixed-building script. These are historical snapshots, not certified correctness examples. Fix any defects you reuse. In particular, MCIO may reorder blockstate properties on reload. Compare parsed block names and property mappings using the standard toolset's `blockstates_equivalent` helper when available. Raw string comparison can report `changed on reload` even after a single write. Overwriting a coordinate is valid when its final state is correct. Ignore property order only; changed names, values or explicit property sets must still fail. ## Objective: maximize valid architectural variety The quantity to maximize is the number of meaningfully different, valid building types expressible by the library's high-level compositions and parameter combinations. Think of the product of the choices across design parameters, subject to their compatibility constraints. This space may be astronomically large. Do not calculate, estimate, enumerate, or report its total size. Spend effort expanding the actual generative system instead. Prioritize diversity in building structure and layout: - Footprint topology, connected wings, their relative placement and proportions, setbacks and asymmetry. - Floor counts, relative wing heights, towers, attics, and the hierarchy of major and minor volumes. - Roof topology, ridge directions, pitch and profiles, cross-gables, dormers and their placement. - Entrance position, circulation routes, stairs, room subdivision, adjacency and functional allocation. - Porches, balconies, pergolas, decks, terraces, market stalls and other compatible extensions. - Root and hill envelopes, recessed or overhanging storeys, stilt arrangements and water relationships. - Site organization: paths, farmyards, fields, wells, windmills, pools, landings and working outdoor spaces. - Terrain relationships, foundations, retaining work, height transitions and the exterior approach. Material families, color choices, weathering, planting and decorative variations also contribute, but have lower priority than structure and layout. Preserve harmonious palettes and appropriate detail density. Different seeds, names, unused parameters, duplicate presets, global rotations or translations alone do not establish additional architectural types. Texture noise on the same building is only minor variety. Simply making dimensions unbounded is not useful diversity: retain the references' human scale and quality. Introduce parameters that change real geometry and can combine across multiple architectural decisions. Make their domains and dependencies explicit. Independent choices multiply variety only when their combinations remain valid. Use conditional domains and geometric constraints for dependent choices rather than pretending every combination is compatible. Broaden the feasible design space instead of claiming combinations the implementation cannot produce. The public API must support both explicit architectural decisions and bounded seeded choices for unspecified or intentionally variable decisions. A lightweight client should be able to define a design space, not merely one fixed building with random texture. Seeded generation must exercise structural choices within that client's declared constraints; it must not silently discard explicit user intent. Document sampling domains and compatibility rules. The example collection must explore the breadth of the eight references and new coherent combinations, without relying on a family-selection switch. ## Public API and lightweight client code - Provide an importable Python package with a documented public API for design composition, seeded generation, validation and export. Calling it must not require the harness CLI or reference scripts. - Users express architectural intent: spatial relationships, room functions, scale or proportion ranges, roof and support rules, site conditions and desired extensions. Avoid requiring absolute block coordinates, voxel placement loops, blockstate manipulation, geometry repair or NBT handling in ordinary client code. - Put substantive planning, constraint resolution, geometry, furnishing and correctness checks in the shared library. Client programs should mostly declare a design and call generation, validation and export. Do not hide long building-specific implementations in example helper files and call the entrypoint thin. - Keep client code genuinely lightweight, but do not code-golf or impose an arbitrary line-count target. Favor a few understandable architectural declarations with sensible defaults over huge configuration dumps, opaque serialized plans or a parameter list that exposes every internal implementation detail. - A short call that selects a prebuilt house is not sufficient abstraction. The API must let users compose new spatial arrangements and combine reusable capabilities without rewriting geometry or validators. - Validation is a reusable public library capability, not assertions embedded in the example clients. It must inspect a generated build and its relevant design constraints and return useful diagnostics, including failed rules and affected components or locations. Support validation of reloaded exports with explicit design metadata where needed; do not rely on hardcoded coordinates for individual examples. - Make extension points clear: adding a reusable component or rule should integrate with shared planning, spatial reservations and validation. Choose abstractions that actually serve the supplied and novel examples; an elaborate framework with weak generated results does not satisfy the task. ## Program design Do not optimize for library code length or worry about how many files or lines its implementation needs. Write as much shared implementation as the result requires, while keeping user programs high-level and lightweight. Judge success by the generated buildings' quality, correctness and diversity across seeds. The code must still follow procedural design principles and remain extensible: use reusable rules, parameterized geometry, explicit constraints and clear module responsibilities so new building types and features can combine with existing ones. More code is acceptable when it improves the results, but a growing collection of hardcoded buildings or seed-specific exceptions is not a procedural solution. Do not sacrifice output quality to keep the implementation short. Useful architectural patterns to consider, combine or adapt: - A compiler-style pipeline: a high-level user specification and seed -> a resolved building plan -> structural geometry -> Minecraft blocks -> validation and export. Keep architectural intent, planning decisions and block placement separate, and retain enough metadata for diagnostics and reproduction. - A constrained building grammar: compose main volumes, wings, towers, roof systems and extensions through parameterized rules with compatibility conditions, drawing design patterns from all eight references. - A compositional architectural API or small embedded DSL: clients describe relationships and constraints, while reusable rules infer the detailed geometry. Support both concrete choices and bounded variation. - A shared component protocol: reusable roofs, root envelopes, supports and terrain adapters declare their inputs, attachment interfaces and validity rules, so users can combine capabilities without selecting a whole-building family or duplicating fixed-building scripts. - A room and circulation graph: plan room adjacency and vertical connections alongside the exterior massing, then realize and verify those connections in actual geometry. - Parameterized components with connection interfaces: roofs, stairs, porches and towers declare their attachment points, occupied space, support requirements and clearance needs. - A shared spatial reservation system: protect passages, doorways, windows and stair headroom so later roof, furnishing and decoration passes cannot obstruct them. - Local constraint solving or bounded candidate refinement: resolve difficult layout and roof interactions at the planning level before committing to detailed block placement. These are suggestions, not a required framework or a checklist of abstractions. Choose a structure that supports a broad, coherent design space and makes quality across samples easier to achieve and extend. ## Library and harness contract - `python generate.py` is a lightweight example client that uses the public library API to generate, validate and export one region to `output.litematic` using a documented default seed. - `python generate.py --seed INTEGER --output PATH` generates a reproducible sample at the requested path. - Every example client lets the user supply a seed and uses the same shared generation and validation API. The default client does not need to represent the entire design space by itself; the library and its composable API must support the breadth demonstrated across clients. - Record each sample's client or design specification, seed and resolved architectural choices so the result can be inspected and reproduced. Document parameter domains and how users express relationships, constraints and seeded variation in their own code. - Use seeded randomness for every random decision. The same seed and configuration must reproduce the same geometry and schematic bytes in the same environment. Follow the toolset's deterministic export rules. - Derive bounds from the chosen design, including roofs, terrain, trees and other exterior features. Keep parameter domains and resource limits explicit and configurable. Never silently clip a building. - Generation must work for arbitrary seeds in the documented seed domain, without seed-specific patches, manual repairs or a list of handpicked successful seeds. Reject invalid explicit parameter combinations with a clear explanation. For a supported, satisfiable client specification, the library's sampler must produce valid configurations within that specification's constraints. - Resolve compatibility during planning and construction. If retries are needed, bound them and provide a valid deterministic recovery strategy. Do not hang, emit broken buildings, or silently fall back to the same fixed building for most seeds. Do not silently change the user's architectural requirements to recover from failure; reject contradictory specifications with actionable diagnostics. - Reused implementation should live in the delivered library modules. Running the finished clients must not depend on dispatching to the eight reference scripts or downloading additional assets. ## Quality required for every sample Treat architectural quality and correctness as constraints on the design space, not costs to trade away for more parameter values. Every admitted combination and every sampled build must satisfy them. Architecture and aesthetics - Preserve the relevant references' recognizable architecture, balanced proportions and composed silhouettes. Frame, infill, foundation, roof, roots and terrain materials must have coherent roles. - Join wings, roofs and extensions intentionally. Roof intersections must be weather-tight, with sensible eaves and no accidental holes or protrusions through occupied rooms. - Align openings and framing with usable rooms. Keep a clear entrance, believable supports and continuous foundations. Balance exterior detail, planting and negative space. - Give all enclosed usable levels and rooms purposes and complete furnishings: sleeping, living, cooking, storage, work or other functions appropriate to that dwelling. Layout must respond to the sampled building. - Compose the whole property: farmyard elements, market spaces, terraces and docks need appropriate space, access and visual hierarchy. An open pergola is distinct from an enclosed roof; a windmill needs clear space for its sails; roots and earth must leave a usable, sealed interior. Site features must adapt to the plan rather than occupying identical fixed coordinates in every sample. Geometry and usability - Use valid vanilla Java 1.21.1 blocks and blockstates, entirely within the schematic bounds. - Every room and occupied level is reachable from the exterior entrance through a continuous walkable route. Maintain at least two blocks of headroom, including over stair treads and at transitions. - Stairs or ladders must be usable and supported. Exterior paths and stairs must connect actual ground to the entrance. For water sites, provide an accessible landing and a continuous usable deck route to the door. Terrain, terraces, supports and foundations must meet without floating joins. Stilt supports must reach the waterbed or ground, overhangs must be supported, and water or hillside material must not invade rooms. - Doors have matching upper and lower halves. Beds have adjacent matching heads and feet. Respect facing, attachment and support requirements for all multi-block furnishings and decorations. - Keep circulation, doors and windows clear of furniture, roofs, chimneys and later decoration passes. - Enclosed interiors must be weather-tight, with deliberate glazed windows and complete doors. Intentional open porches, balconies and exterior spaces remain distinct from enclosed rooms. - Light every room near its usable floor, and provide coherent exterior lighting along the approach and around important features. ## Validate the system across its design space Do not validate only the default client or the easiest composition. Choose a finite, diverse seed corpus and targeted client specifications that cover every major architectural capability, domain boundaries and difficult component interactions. Expand or repair the generator when these reveal failures. This is sampled and targeted validation, not an exhaustive enumeration of the parameter space or proof that all combinations were tested. - Write `test/test_*.py` tests for the public API and shared validators: determinism, bounds, valid states, support and multi-block consistency, room and level reachability, headroom, envelope integrity and clear openings. Exercise the same library through multiple independent lightweight clients. - Verify that validators reject deliberately broken geometry, such as blocked stairs, unsupported features and mismatched door halves, with meaningful diagnostics. Passing checks on valid examples alone does not show that the validation library detects defects. - Demonstrate API composition by adding new lightweight clients after the shared abstractions are in place, combining existing capabilities into buildings beyond the eight references without changing library code. Check that these clients need no custom geometry routines, validator copies or block-level repairs. - Check actual placed or reloaded geometry. Declared room connections or planned stairs alone do not prove the final blocks are traversable after roofs and furnishing have been placed. - Compare round trips semantically, ignoring property order while detecting changed names or properties. - Check structural diversity in the sampled corpus using actual massing, roof or room-layout signatures. Include variation across seeds of one client specification and across distinct client compositions. Different client names alone are not evidence of procedural variation. Exclude material noise and incidental vegetation from structural signatures. Distinct seeds or file hashes alone do not prove architectural variety. Report only observed sample diversity, never a speculative total number of possible buildings. - Supply a lightweight client composition demonstrating the architectural characteristics of each of the eight references, with at least two seeds per client producing meaningful structural or layout changes. Also include novel compositions using the same library. Cover compact and large examples, difficult roof joins, roots around rooms, buried floors, stilt supports and indoor/outdoor circulation with targeted cases. - Render a representative spread covering all eight references and novel compositions from multiple exterior views, with cutaways for interior circulation and furnishing. Inspect the images and fix visible problems. Do not present only cherry-picked successful builds while leaving reproducible failures unresolved. - Keep tests bounded and repeatable. Temporary test artifacts belong under `test/` and must be cleaned up. ## Deliverables All inside `/work/generator/workspace`: - An importable Python package: the shared procedural generation and validation library. Choose a clear package name and module structure. Keep the public architectural API separate from implementation details. - `generate.py`: a lightweight default client using that public API, rerunnable from a clean directory with the library available as documented. The standard toolset seeds a writable `generate.py` with placement, export and reload helpers; move reusable implementation into the library as needed. - `examples/`: lightweight, runnable client programs demonstrating all eight reference architectures and additional new compositions. Each accepts a user-controlled seed. They must demonstrate the actual public abstractions rather than call hidden whole-building presets. Provide batch generation instructions. - `output.litematic`: the default client's default-seed build. This exact path remains the harness contract. - `GENERATOR.md`: library quickstart, public API, short client examples, parameter domains and dependencies, seed and sampling behavior, validation API and diagnostics, extension points and design rationale. Explain how the eight references motivated reusable abstractions and how users compose new buildings without modifying the library. Include validation commands and known limitations. Do not compute the total combination count. - `samples/`: a finite, representative collection of generated builds, with client/specification, seed and resolved parameters recorded in a manifest. Provide a rerunnable command or script for the sample batch. - `test/test_*.py`: passing tests covering the public library, shared validators and varied client compositions. - `previews/`: labeled exterior renders and cutaways for representative samples. Spend the budget on the shared library, clear abstractions, lightweight clients, valid architectural diversity, renders and tests. Do not spend it enumerating parameter products. State plainly what is complete and what remains unfinished. Environment, available libraries and rendering tools are described in the workspace `AGENTS.md`.

04 · 运行、Token 与结果

记录值
运行目录runs/standard/proc-8/20260925T110041
模型 / reasoning effort / toolsetgpt-6-astra / xhigh / standard
Agent 时间5,038 秒,83 分 58 秒;不含后续独立评分耗时
请求 / shell 命令110 / 125
输入 Token(含缓存)15,281,964
缓存输入 / 非缓存输入14,727,168 / 554,796
输出 Token(含推理)146,809,其中 reasoning 58,242
累计总 Token15,428,773;重复上下文在每次请求中累计,不能当成独有源码或新内容
进程 / 评分agent exit 0;harness passed;schematic_valid true;受保护工具栈无修改
默认产物seed 20260925;38 × 35 × 36;6,493 个非空气方块;51 种方块名
请求累计 Token 分解。推理 Token 已包含在输出中,不重复相加。

以上来自 harness report 与 session usage。此次报告没有再次运行模型。

05 · 共享库与轻量客户端

层次实现职责
建筑声明api.py不可变 dataclass,Choice / Span,命名体量、房间功能、屋顶、场地
规划plan.py解析 seed;过滤局部不可行选项;有界回溯;体量连接与占地分配
几何编译build.py / roofs.py / site.py地形、外壳、屋顶、分区、交通、开口、附属空间、室外道路
可达与家具navigation.py / furnish.py预留通道、房间通行、按尺度布置家具、库存、灯具与植物
方块与输送blocks.py合法状态、稀疏网格、MCIO 导出、NBT、设计 sidecar、语义读回
公共验证validate.py检查实际方块与设计元数据;诊断规则、组件和坐标
扩展接口components.py注册共享场地组件与额外验证规则

构造次序保护了交通空间:先规划和建立外壳、楼梯、门及道路,再保留通道、摆放家具和植物。Building.export() 调用验证;Building.load() 可将导出文件及相邻 JSON 恢复为可检查对象。

三翼庭院是一个可读的抽象测试:下面全部代码只有建筑声明,没有放方块循环、几何补丁或专属验证器。

完整三翼庭院客户端
# A new composition: three connected masses frame a sheltered entrance court. from hearthwright import Attachment, Choice, Design, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Three gables court', volumes=( Volume('hall', width=Choice(23, 25), depth=Choice(11, 13), levels=(Level( ('living', 'kitchen', 'study')), Level(('sleeping', 'storage'), layout='cross')), roof=Roof(ridge='x', pitch=.5)), Volume('work', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'front'), levels=(Level(('workshop', 'storage')),), roof=Roof(ridge='z', pitch=1.0)), Volume('guest', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'back'), levels=(Level(('sleeping', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='plaster', frame='spruce', roof='slate', accent='blue'), site=Site(margin=3, planting='sparse', paving='yard'), attachments=(Attachment('porch', 'hall', width=5, depth=3, cover='pergola'),), ) if __name__ == '__main__': run(DESIGN)

扩展也有边界:屋顶公开词汇目前只有 gable / hip;新屋顶算法仍需接入规划与几何模块。SiteComponent 可注册新的场地能力,不能据此宣称所有建筑机制都已经插件化。

06 · 多样性:观察到了什么

存档审计覆盖 13 个客户端 × seed 0–63,共 832 次生成。下图比较实际记录的规划签名和放置几何签名。每个柱子都来自有限样本,没有外推设计空间总数。

64 个 seed 下的观察值,可点击图例切换指标。规划差异不一定变成新的结构几何。
客户端规划签名几何签名生成次数
运河药剂师住宅8864
三翼庭院161664
山坡工作室313164
地中海别墅453164
商人庄园454564
果园住宅452464
红顶农庄311664
树根住宅383864
砂岩小屋313164
水上高脚小屋16864
架空庄园313164
石砌小屋8864
林间书库453164

几何指标的具体口径:Compiler.geometry_signature() 在家具和植物之前取样,在各体量外扩两格的范围内、底层地面以上,记录 solid / glass / stair / slab / door / ladder 等角色以及部分朝向、半部属性,然后哈希。材质颜色被归并,因此比文件哈希更接近结构差异。

仍不是完整的建筑种类判定器。它可能漏掉较远的场地特征、某些树根或外部设施;没有完整的旋转等价归并,也不能度量两个结构“有多不同”。13 个客户端的这些计数不能直接相加作为全局去重结果。

审美上的观察:别墅和三翼庭院的体量、屋顶、附属空间关系可辨;树根住宅形成了独立于普通矩形房屋的外壳。但多个常规建筑仍共享相似的直线体量、山墙和立面节奏。屋顶词汇、分区形式与有限示例域仍限制了变化。增大 seed 数本身不会消除这些限制。

07 · 26 份保存样本

每个客户端保存 seed 0 与 1,均记录声明、解析参数、生成检查和读回检查。下载全部样本和元数据 · 完整 manifest。

客户端SeedXYZ方块房间几何签名下载
地中海别墅049 × 19 × 416836366e0227542e6建筑 · 元数据
地中海别墅145 × 18 × 3957924e64bfdb1f1dc建筑 · 元数据
商人庄园038 × 32 × 54906162f08452acf1c建筑 · 元数据
商人庄园136 × 32 × 52816672416f16bd3a7建筑 · 元数据
红顶农庄053 × 33 × 346127291edcba3c3ce建筑 · 元数据
红顶农庄151 × 28 × 325204336c7a0441237建筑 · 元数据
树根住宅037 × 25 × 45702225e1a449c0244建筑 · 元数据
树根住宅133 × 25 × 4053142087898ff2dd4建筑 · 元数据
砂岩小屋043 × 20 × 41918132631cabf77df建筑 · 元数据
砂岩小屋141 × 20 × 3980344129a2da964f4建筑 · 元数据
水上高脚小屋035 × 17 × 3748881f97c061878dd建筑 · 元数据
水上高脚小屋133 × 17 × 3440711126f5ba4f824建筑 · 元数据
架空庄园038 × 41 × 387375458e319651c41建筑 · 元数据
架空庄园136 × 39 × 3663124985715021f11建筑 · 元数据
石砌小屋023 × 19 × 2822162014895e58bf8建筑 · 元数据
石砌小屋121 × 18 × 2617112e92f81eddb39建筑 · 元数据
运河药剂师住宅047 × 18 × 3971352cffa054020ca建筑 · 元数据
运河药剂师住宅145 × 18 × 3966712dd100a10d40c建筑 · 元数据
林间书库037 × 22 × 40500837c1a6ead7f28建筑 · 元数据
林间书库135 × 22 × 38446731a2b5b888c45建筑 · 元数据
果园住宅043 × 20 × 3748662a9a02750037d建筑 · 元数据
果园住宅141 × 17 × 3947801caaef198a647建筑 · 元数据
山坡工作室043 × 17 × 405443262bfdf0cf48e建筑 · 元数据
山坡工作室141 × 19 × 3849342cb3a1b79a689建筑 · 元数据
三翼庭院037 × 18 × 37668356f509fd9e90e建筑 · 元数据
三翼庭院135 × 18 × 355854568bc1de1bd7d建筑 · 元数据

09 · 验证覆盖与边界

独立 harness 在 agent 结束后运行测试:225 passed in 42.30s。agent 存档测试耗时为 42.75 秒,两次结果一致,耗时不同。

........................................................................ [ 32%] ........................................................................ [ 64%] ........................................................................ [ 96%] ......... [100%] 225 passed in 42.30s
证据覆盖不能推出
公共 API / corpus13 个客户端、固定 seed、signed 64-bit 两端、四层、最大占地与坡度等边界任意未来组合均可生成
破坏测试楼梯朝向/净空、门与床双半部、桩基、梯背、玻璃、屋顶、灯光、家具、库存、独立不可达区域所有 Minecraft 运行时规则完整模拟
832 次审计13 个客户端各 64 seed,存档 failures 为空不同 seed 必然是不同建筑;每个参数组合都满足审美
26 次样本保存/读取完整非空气坐标集合与状态语义;导出后再次几何验证仅凭字符串完全相同就等于语义正确

方块状态比较使用解析后的名字与属性映射;仅忽略属性顺序,名称、属性值或显式属性集合变化仍失败。覆盖同一格不是错误本身。这里的检查修复与先前 ref8 的 changed on reload 问题一致。

验证依赖设计 JSON 里的房间、通道和意图,所以元数据随建筑一起下载。没有模拟完整游戏物理、精确光照传播和红石。生成侧提供的意图也可能有遗漏;通过规则不是独立审美认证。832 次审计为原 agent 产物记录,本报告未重新执行该审计。

10 · 失败与修复轨迹

125 条命令中有 4 条记录了非零退出码。日志尾部与后续检查给出三个明确问题,最终测试和审计通过:

证据位置现象最终代码 / 后续证据
#041扩大家具后出现 furnishing.changed,13 项测试失败,家具被覆盖家具保护与放置逻辑继续修订;最终家具破坏测试与 corpus 通过
#058–062新增入口照明检查后出现 lighting.approach;测试 31 项失败,审计退出 1最终构造管线显式执行 approach_lighting,完整审计 failures 为空
#110–115树根住宅外侧平台没有接到可通行地形,3 项 reachability.terrain 失败入口通路需越过真实土丘与根系;后续重生成、测试通过

以下是归档的命令及输出尾部,不是完整终端逐字记录。某些输出被重定向到文件;exit 0 也不代表输出里没有失败。时间戳与命令编号可定位修复上下文。

#001 · 2026-09-25T15:01:19.227Z · exit=0 · pwd && rg --files -g 'AGENTS.md' -g 'generate.py' -g 'README.md' -g '*render*' . /work | head -100
pwd && rg --files -g 'AGENTS.md' -g 'generate.py' -g 'README.md' -g '*render*' . /work | head -100

日志保留的输出末尾

/work/generator/workspace ./references/stilted-manor/generate.py ./references/red-roof-farmstead/generate.py ./references/stilt-water-cabin/generate.py ./references/merchant-manor/generate.py ./references/sandstone-cottage/generate.py /work/generator/workspace/references/stilted-manor/generate.py ./references/mediterranean-villa/generate.py ./references/README.md /work/generator/workspace/references/README.md /work/generator/workspace/references/red-roof-farmstead/generate.py ./references/stone-cottage/generate.py /work/generator/workspace/references/stilt-water-cabin/generate.py /work/generator/workspace/references/merchant-manor/generate.py ./references/root-house/generate.py ./generate.py ./AGENTS.md /work/generator/workspace/references/sandstone-cottage/generate.py /work/generator/workspace/generate.py /work/generator/workspace/AGENTS.md /work/generator/workspace/references/mediterranean-villa/generate.py /work/generator/workspace/references/root-house/generate.py /work/generator/workspace/references/stone-cottage/generate.py
#002 · 2026-09-25T15:01:19.300Z · exit=0 · cat /work/AGENTS.md && cat generate.py && cat references/README.md
cat /work/AGENTS.md && cat generate.py && cat references/README.md

日志保留的输出末尾

ad.jpg` | `runs/exp8/red-roof-farmstead/work/generator/workspace/reference_red_roof_farmstead.jpg` | `cecfd8ad0047913471e105c5065cce44146779ff2a1aa7631ac7e452a51fa2fb` | | `references/root-house/generate.py` | `runs/exp8/root-house/work/generator/workspace/generate.py` | `d24c54dfa3d2bd1f7b2d6e485c9bb555d459365e51eb56811147cec108042d24` | | `reference_root_house.jpg` | `runs/exp8/root-house/work/generator/workspace/reference_root_house.jpg` | `65d82aa169f29a14feb45609180a75eae6f6a3005f408d6a5a5a44026c6d0b6b` | | `references/sandstone-cottage/generate.py` | `runs/exp8/sandstone-cottage/work/generator/workspace/generate.py` | `b895584fa4db7eaa5861c78f87fc666b72c350c4617b79587f5fd9d8ba89b1c2` | | `reference_sandstone_cottage.jpg` | `runs/exp8/sandstone-cottage/work/generator/workspace/reference_sandstone_cottage.jpg` | `91f219ccfaabab92f430db6847f9737930cbbc7adc7c29dec560a793e53ec409` | | `references/stilt-water-cabin/generate.py` | `runs/exp8/stilt-water-cabin/work/generator/workspace/generate.py` | `a8d11827e71a5765904af86cf47ebeef399071d53bee0ef32338828463ae5036` | | `reference_stilt_water_cabin.jpg` | `runs/exp8/stilt-water-cabin/work/generator/workspace/reference_stilt_water_cabin.jpg` | `f5f82de38845f10667852b8967a5854ba8574533a16a4cfaa20b699df47e8aee` | | `references/stilted-manor/generate.py` | `runs/exp8/stilted-manor/work/generator/workspace/generate.py` | `affdb6cba881a7b280a81833ba3fbd906e5730dea82d0cc37a4b1ae364868d3e` | | `reference_stilted_manor.jpg` | `runs/exp8/stilted-manor/work/generator/workspace/reference_stilted_manor.jpg` | `8b435bffc278bbbb64e79e4c4f2ed9188124a4c73557f6667fce46b266d29923` | | `references/stone-cottage/generate.py` | `runs/exp8/stone-cottage/work/generator/workspace/generate.py` | `da24c75f03bdd7a17f9201f31f24877301881beeee0d66cead309b269dd7f412` | | `reference_stone_cottage.jpg` | `runs/exp8/stone-cottage/work/generator/workspace/reference_stone_cottage.jpg` | `80116881f0af3f7d1a5d3937ecfb14a9afb99b42507cd047a0fb2239bbe56a5a` |
#003 · 2026-09-25T15:01:27.206Z · exit=0 · wc -l references/*/generate.py && cat AGENTS.md && cat /opt/tools/template_render.mjs
wc -l references/*/generate.py && cat AGENTS.md && cat /opt/tools/template_render.mjs

日志保留的输出末尾

Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [47, 52, 59, 255]; const VIEWS = [ { name: 'front', azimuth: 210, elevation: 18, radius: 3.3 }, { name: 'rear', azimuth: 35, elevation: 22, radius: 3.3 }, { name: 'side', azimuth: 120, elevation: 14, radius: 3.3 }, { name: 'top', azimuth: 210, elevation: 62, radius: 3.4 }, ]; // Resolve every path while the working directory is still the caller's. const absoluteInput = path.resolve(INPUT); const absoluteOutDir = path.resolve(OUT_DIR); fs.mkdirSync(absoluteOutDir, { recursive: true }); // The renderer looks for its resource cache relative to the working directory, so move there first, // and only then create the scene. process.chdir(RENDER_ROOT); const scene = createRenderScene({ width: SIZE, height: SIZE }); for (const view of VIEWS) { const result = renderLitematicWithMetadata(absoluteInput, { width: SIZE, height: SIZE, fov_deg: 30, radius: view.radius, nRandomAzimuths: 0, azimuthInit: 0, elevationInit: 0, azimuthsDeltas: [view.azimuth], elevationsDeltas: [view.elevation], ambientIntensities: [AMBIENT], lightDirections: [LIGHT_DIRECTION], mirror: [false], layout: [[0]], limit: 512, seed: 0, opaque: false, }, scene); // Flatten onto an opaque background so shaded faces keep their contrast. for (let i = 0; i < result.png.data.length; i += 4) { const alpha = result.png.data[i + 3] / 255; for (let channel = 0; channel < 3; channel++) { result.png.data[i + channel] = Math.round(result.png.data[i + channel] * alpha + BACKGROUND[channel] * (1 - alpha)); } result.png.data[i + 3] = 255; } const target = path.join(absoluteOutDir, `${view.name}.png`); fs.writeFileSync(target, PNG.sync.write(result.png)); console.log(`wrote ${target} (${SIZE}x${SIZE}, azimuth ${view.azimuth}, elevation ${view.elevation})`); }
#004 · 2026-09-25T15:01:27.254Z · exit=0 · rg -n '^class |^def |^ def |CONFIG|PALETTE|room|stairs|roof|root' references/*/generate.py
rg -n '^class |^def |^ def |CONFIG|PALETTE|room|stairs|roof|root' references/*/generate.py

日志保留的输出末尾

nge walkable. references/sandstone-cottage/generate.py:343: self.stair(x, 3, 5, "minecraft:stone_brick_stairs", "south") references/sandstone-cottage/generate.py:347: self.put(x, 8, 11, "minecraft:rooted_dirt") references/sandstone-cottage/generate.py:351: self.stair(x, 7, 11, "minecraft:spruce_stairs", "south", "top") references/sandstone-cottage/generate.py:360: self.put(x, 3, z, "minecraft:rooted_dirt") references/sandstone-cottage/generate.py:389: def interiors(self) -> None: references/sandstone-cottage/generate.py:403: self.stair(x, 10, 15, "minecraft:spruce_stairs", "south") references/sandstone-cottage/generate.py:436: self.stair(x, 10, 16, "minecraft:dark_oak_stairs", "south") references/sandstone-cottage/generate.py:437: self.stair(x, 10, 18, "minecraft:dark_oak_stairs", "north") references/sandstone-cottage/generate.py:474: def build(self) -> "Build": references/sandstone-cottage/generate.py:479: self.roof() references/sandstone-cottage/generate.py:484: def export(self, path: Path) -> Path: references/sandstone-cottage/generate.py:494: name=CONFIG["name"], references/sandstone-cottage/generate.py:495: author=CONFIG["author"], references/sandstone-cottage/generate.py:496: description=CONFIG["description"], references/sandstone-cottage/generate.py:497: minecraft_data_version=CONFIG["data_version"], references/sandstone-cottage/generate.py:515: nbt["Regions"][CONFIG["name"]]["Entities"] = NBTList[Compound]([boat]) references/sandstone-cottage/generate.py:519: def verify(self, path: Path) -> Dict[str, object]: references/sandstone-cottage/generate.py:544:def main() -> None: references/sandstone-cottage/generate.py:547: parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) references/sandstone-cottage/generate.py:548: parser.add_argument("--seed", type=int, default=CONFIG["seed"])
#005 · 2026-09-25T15:01:34.756Z · exit=0 · sed -n '149,531p' references/mediterranean-villa/generate.py
sed -n '149,531p' references/mediterranean-villa/generate.py

日志保留的输出末尾

# Front entrance, with a taller recessed stone surround. for x, hinge in ((21, "left"), (22, "right")): self.door(x, 5, 16, "south", hinge) self.p(x, 7, 16, "light_blue_stained_glass") for x in (20, 23): self.fill((x, 5, 17, x, 8, 17), "smooth_sandstone") self.p(x, 9, 17, "chiseled_sandstone") self.fill((20, 9, 17, 23, 9, 17), "smooth_sandstone") self.fill((21, 8, 17, 22, 8, 17), "smooth_sandstone") for x in (20, 23): self.planter(x, 5, 18, "potted_flowering_azalea_bush") # Cool blue plaster panels recall the reference's blue-and-white elevations. for bounds in ((7, 11, 5, 7, 15, 15), (24, 11, 5, 24, 15, 15), (8, 11, 16, 12, 15, 16), (31, 5, 9, 31, 9, 14), (25, 5, 15, 30, 9, 15)): self.mix(bounds, ["light_blue_concrete"] * 9 + ["light_blue_terracotta"]) self.window(10, 6, 16, "south") self.window(10, 12, 16, "south") for x, hinge in ((14, "left"), (15, "right")): self.door(x, 11, 16, "south", hinge) self.p(x, 13, 16, "light_blue_stained_glass") self.window(20, 12, 16, "south") for y in (6, 12): self.window(10, y, 4, "north") self.window(7, y, 7, "west") self.window(7, y, 12, "west") self.window(20, 6, 4, "north") self.window(16, 12, 4, "north") self.window(21, 12, 4, "north", width=1) self.window(24, 12, 7, "east") self.window(27, 6, 15, "south") self.window(31, 6, 11, "east") self.window(27, 6, 8, "north") # Upper terrace, with real support columns and open railings. self.fill((7, 10, 17, 16, 10, 21), "oak_planks") for x in (7, 16): self.fill((x, 5, 21, x, 9, 21), "quartz_pillar", axis="y") self.fill((7, 9, 21, 16, 9, 21), "smooth_quartz") for x in range(8, 16): self.p(x, 11, 21, "oak_fence", east="true", west="true")
#006 · 2026-09-25T15:01:34.770Z · exit=0 · sed -n '150,470p' references/merchant-manor/generate.py
sed -n '150,470p' references/merchant-manor/generate.py

日志保留的输出末尾

dow(7, 18, 17, 1, 3, 'west') self.window(29, 18, 21, 2, 3, 'east') for x in (6, 30): for z in (11, 28): self.p(x, 18, z, 'spruce_slab', type='bottom') self.roof_heights = heights def tower(self): """Add the central study tower, gabled crown, and smaller western spire.""" # The tower cuts through the main roof; its walls replace the roof intersections. self.fill((15, 16, 17, 23, 32, 25), 'air') for bounds in [(15, 16, 17, 23, 32, 17), (15, 16, 25, 23, 32, 25), (15, 16, 18, 15, 32, 24), (23, 16, 18, 23, 32, 24)]: self.mix(bounds, PLASTER) for x in (15, 19, 23): for z in (17, 25): self.fill((x, 16, z, x, 32, z), 'stripped_spruce_log') for x in (15, 23): for z in (21,): self.fill((x, 16, z, x, 32, z), 'stripped_spruce_log') for y in (22, 28, 32): for z in (17, 25): self.fill((15, y, z, 23, y, z), 'dark_oak_log', axis='x') for x in (15, 23): self.fill((x, y, 17, x, y, 25), 'dark_oak_log', axis='z') for y in (22, 28): self.fill((16, y, 18, 22, y, 24), 'spruce_planks') self.door(15, 16, 18, facing='west') self.door(23, 16, 20, facing='east') for y, height in ((24, 3), (29, 3)): for x in (17, 21): self.window(x, y, 17, 1, height) self.window(x, y, 25, 1, height, 'south') for x, side in ((15, 'west'), (23, 'east')): self.window(x, 26 if y == 24 else y, 19, 1, 2 if y == 24 else height, side) self.window(x, y, 23, 1, height, side) for x in range(14, 25): h = 33 + (min(x - 14, 24 - x) * 6) // 5 for z in (17, 25): self.mix((x, 33, z, x, max(33, h - 1), z), PLASTER) if x == 19: self.fill((x, 33, z, x, h - 1, z), 'stripped_spruce_log')
#007 · 2026-09-25T15:01:34.819Z · exit=0 · sed -n '149,470p' references/red-roof-farmstead/generate.py
sed -n '149,470p' references/red-roof-farmstead/generate.py

日志保留的输出末尾

39, 37)]: self.fill((x, 3, z, x, 4, z), 'oak_log', axis='y') self.p(x, 5, z, 'spruce_slab', type='bottom') self.p(35, 3, 20, 'spruce_fence_gate', facing='south', open='true', in_wall='false') self.p(36, 3, 20, 'spruce_fence_gate', facing='south', open='true', in_wall='false') self.fill((30, 3, 23, 32, 3, 25), 'hay_block', axis='y') self.fill((31, 4, 23, 32, 4, 24), 'hay_block', axis='x') self.p(32, 5, 23, 'hay_block', axis='z') self.p(30, 3, 26, 'barrel', facing='up', open='false') self.p(30, 4, 26, 'lantern', hanging='false') # Solid tapering timber tower stands five blocks clear of the house wall. self.mix((34, 3, 10, 37, 3, 13), ['stone_bricks', 'cobblestone']) self.fill((34, 4, 10, 37, 7, 13), 'spruce_planks') for x in (34, 37): for z in (10, 13): self.fill((x, 4, z, x, 9, z), 'oak_log', axis='y') self.fill((34, 8, 10, 37, 8, 13), 'oak_log', axis='x') self.fill((35, 9, 11, 36, 14, 12), 'stripped_spruce_log', axis='y') self.fill((34, 15, 10, 37, 15, 13), 'spruce_slab', type='bottom') self.fill((35, 13, 12, 35, 13, 15), 'oak_log', axis='z') # Four offset lattice paddles create a clearly readable pinwheel silhouette. for d in range(-5, 6): if d: self.p(35 + d, 13, 15, 'stripped_oak_log', axis='x') self.p(35, 13 + d, 15, 'stripped_oak_log', axis='y') for d in range(3, 6): for w in (1, 2): for x, y in [(35 + w, 13 + d), (35 + d, 13 - w), (35 - w, 13 - d), (35 - d, 13 + w)]: self.p(x, y, 15, 'oak_fence') self.p(35, 13, 16, 'stripped_oak_log', axis='z') self.p(35, 4, 14, 'grindstone', face='floor', facing='south') self.p(35, 3, 14, 'cobblestone') self.p(36, 3, 15, 'barrel', facing='up', open='false') self.p(36, 4, 15, 'lantern', hanging='false')
#008 · 2026-09-25T15:01:41.812Z · exit=0 · sed -n '245,680p' references/merchant-manor/generate.py
sed -n '245,680p' references/merchant-manor/generate.py

日志保留的输出末尾

self.fill((16, 29, 21, 16, 29, 23), 'spruce_stairs', facing='west') self.p(19, 29, 18, 'cartography_table') self.p(19, 30, 18, 'yellow_candle', candles='2', lit='true') self.p(21, 29, 21, 'spruce_planks') self.frame(21, 29, 21, 'spyglass') self.p(16, 29, 24, 'spruce_planks') self.light(16, 30, 24) self.fill((18, 29, 21, 19, 29, 23), 'green_carpet') # The tiny turret has a useful lookout and its own light. self.p(8, 24, 20, 'spruce_planks') self.light(8, 25, 20) self.p(8, 24, 22, 'spruce_stairs', facing='west') def market_and_chimney(self): """Build the striped street stall, left loading shelter and smoking brick stack.""" for x in (23, 31): self.fill((x, 1, 6, x, 5, 6), 'spruce_fence', north='false', south='false', east='false', west='false') self.p(x, 1, 6, 'stripped_spruce_log') for x in range(23, 32): color = 'red_wool' if (x - 23) // 2 % 2 == 0 else 'white_wool' for z in range(6, 11): y = 5 if z <= 7 else 6 self.p(x, y, z, color) self.p(x, 4, 6, color) self.fill((23, 6, 10, 31, 6, 10), 'spruce_planks') self.fill((24, 1, 7, 30, 1, 7), 'spruce_planks') self.fill((24, 2, 7, 30, 2, 7), 'barrel', facing='up', open='false') for x, block in ((24, 'pumpkin'), (25, 'melon'), (29, 'hay_block')): self.p(x, 3, 7, block) self.p(27, 3, 7, 'flower_pot') self.light(30, 3, 7) self.frame(26, 2, 7, 'bread') self.frame(28, 2, 7, 'apple') # A gabled trade sign, solidly attached to the stall beam. self.fill((25, 7, 10, 29, 7, 10), 'dark_oak_planks') self.frame(27, 7, 10, 'emerald') # Left lean-to, loaded with coopered casks and bundled bales. for x in (2, 8): for z in (5, 10): self.fill((x, 1, z, x, 5, z), 'stripped_spruce_log')
#009 · 2026-09-25T15:01:41.818Z · exit=0 · sed -n '158,390p' references/root-house/generate.py
sed -n '158,390p' references/root-house/generate.py

日志保留的输出末尾

for y, half in ((3, 'lower'), (4, 'upper')): self.put(x, y, 8, 'minecraft:spruce_door', facing='north', hinge=hinge, half=half, open='false', powered='false') self.put(x, 5, 8, 'minecraft:brown_stained_glass') for x in (9, 15): self.put(x, 2, 5, 'minecraft:dark_oak_log', axis='y') for y in range(3, 6): self.put(x, y, 5, 'minecraft:spruce_fence', north='false', south='false', east='false', west='false', waterlogged='false') self.box((9, 6, 5, 15, 6, 7), 'minecraft:spruce_planks') self.box((9, 6, 4, 15, 6, 4), 'minecraft:spruce_slab', type='top', waterlogged='false') for x in range(9, 16): self.put(x, 7, 4, 'minecraft:spruce_trapdoor', facing='north', half='bottom', open='false', powered='false', waterlogged='false') self.put(12, 5, 5, 'minecraft:lantern', hanging='true', waterlogged='false') # Five-block octagonal frame with a cross-shaped glazed aperture. for dy in range(-2, 3): for dx in range(-2, 3): if abs(dx) + abs(dy) > 3: continue x, y = 12 + dx, 13 + dy if abs(dx) + abs(dy) <= 1: self.put(x, y, 9, 'minecraft:light_gray_stained_glass') self.put(x, y, 10, AIR) else: self.put(x, y, 9, 'minecraft:stripped_spruce_log', axis='z') for x, y, facing, half in [(10, 12, 'east', 'bottom'), (10, 14, 'east', 'top'), (14, 12, 'west', 'bottom'), (14, 14, 'west', 'top')]: self.put(x, y, 8, 'minecraft:dark_oak_stairs', facing=facing, half=half, shape='straight', waterlogged='false') self.box((11, 10, 8, 13, 10, 8), 'minecraft:dark_oak_slab', type='top', waterlogged='false') def container(self, position: Position, block: str, facing: str, items: list[tuple[str, int]]) -> None: """Place a usable storage block and record its Java 1.21 item inventory.
#010 · 2026-09-25T15:01:41.823Z · exit=0 · sed -n '170,474p' references/sandstone-cottage/generate.py
sed -n '170,474p' references/sandstone-cottage/generate.py

日志保留的输出末尾

(13, 17), (21, 15), (24, 20)): self.lantern(x, 13, z, hanging=True) self.box((11, 13, 17, 15, 13, 17), "minecraft:stripped_spruce_log", axis="x") self.lantern(13, 12, 17, hanging=True) # Workbench row, storage, and a clear landing in the cellar. for x in range(5, 10): self.put(x, 5, 21, "minecraft:barrel", facing="north", open="false") self.put(6, 5, 21, "minecraft:crafting_table") self.put(7, 5, 21, "minecraft:smithing_table") self.put(8, 5, 21, "minecraft:stonecutter", facing="north") self.lantern(5, 6, 21) self.put(9, 6, 21, "minecraft:potted_cactus") for x in (6, 8): self.put(x, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false") self.put(5, 5, 17, "minecraft:barrel", facing="east", open="false") self.lantern(5, 6, 17) self.put(14, 5, 10, "minecraft:barrel", facing="north", open="false") self.lantern(14, 6, 10) self.box((11, 5, 15, 13, 5, 17), "minecraft:brown_carpet") for x in (18, 19, 21, 22): self.put(x, 5, 21, "minecraft:barrel", facing="north", open="false") self.put(x, 6, 21, "minecraft:barrel", facing="north", open="false") self.put(18, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false") self.put(21, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false") self.put(23, 5, 21, "minecraft:crafting_table") self.lantern(23, 6, 21) self.put(23, 5, 13, "minecraft:barrel", facing="north", open="false") self.lantern(23, 6, 13) self.box((18, 8, 17, 23, 8, 17), "minecraft:stripped_spruce_log", axis="x") self.lantern(20, 7, 17, hanging=True) self.lantern(12, 8, 19, hanging=True) self.box((10, 8, 16, 14, 8, 16), "minecraft:stripped_spruce_log", axis="x") self.lantern(11, 7, 16, hanging=True) def build(self) -> "Build":
#011 · 2026-09-25T15:01:48.938Z · exit=0 · sed -n '158,570p' references/stilted-manor/generate.py
sed -n '158,570p' references/stilted-manor/generate.py

日志保留的输出末尾

false') self.candle(26, 18, 20) self.put(7, 17, 10, 'minecraft:barrel', facing='up', open='false') self.candle(7, 18, 10) # Lookout writing shelf and seating leave the ladder landing free. self.put(16, 28, 16, 'minecraft:bookshelf') self.candle(16, 29, 16, 4) self.stairs(16, 28, 14, 'west') self.put(18, 31, 14, 'minecraft:spruce_planks') self.lantern(18, 30, 14, True) def circulation(self): """Cut generous two-wide stairs and a continuous supported lookout ladder.""" for x0, z0, bottom, top in [(10, 13, 2, 9), (7, 12, 9, 16)]: for index in range(top - bottom): z, y = z0 + index, bottom + index + 1 for x in [x0, x0 + 1]: self.box((x, bottom + 1, z, x, y, z), 'minecraft:spruce_planks') self.box((x, y + 1, z, x, y + 3, z), 'minecraft:air') self.stairs(x, y, z, 'south') for x in [x0, x0 + 1]: self.box((x, top + 1, z0 + top - bottom, x, top + 2, z0 + top - bottom), 'minecraft:air') # Guard the long open edges of both stairwells. for x, y, lo, hi in [(9, 10, 17, 19), (12, 10, 17, 19), (9, 17, 16, 18)]: for z in range(lo, hi + 1): self.put(x, y, z, 'minecraft:spruce_fence', north=str(z > lo).lower(), south=str(z < hi).lower(), east='false', west='false', waterlogged='false') self.timber((18, 17, 17, 18, 29, 17)) for y in range(17, 30): self.put(18, y, 16, 'minecraft:ladder', facing='north', waterlogged='false') # A landing next to the top rung is open and two blocks tall. self.box((17, 28, 16, 17, 30, 16), 'minecraft:air') def spruce_tree(self, x, z, height, radius): """Grow a narrow, tiered spruce with persistent, connected foliage.""" self.box((x, 2, z, x, height, z), 'minecraft:spruce_log', axis='y') for y in range(5, height + 2, 3):
#012 · 2026-09-25T15:01:48.951Z · exit=0 · sed -n '164,367p' references/stone-cottage/generate.py
sed -n '164,367p' references/stone-cottage/generate.py

日志保留的输出末尾

name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) nbt = schematic.write_to_nbt() region = next(iter(nbt["Regions"].values())) region["TileEntities"] = self.block_entities() NBTFile(nbt).save(path) return path def block_entities(self) -> NBTList: """Create bed and container NBT, including a few useful cottage supplies. Returns: NBTList: Block entities with region-relative coordinates. """ entities = [] supplies = {(4, 2, 10): ("minecraft:bread", 6), (10, 2, 10): ("minecraft:coal", 12), (5, 7, 10): ("minecraft:wheat_seeds", 16), (9, 7, 10): ("minecraft:oak_log", 8)} for (x, y, z), state in sorted(self.placed.items()): name = state.split("[")[0].removeprefix("minecraft:") kind = "bed" if name.endswith("_bed") else name if kind not in ("bed", "furnace", "smoker", "barrel"): continue tag = Compound({"id": String(f"minecraft:{kind}"), "x": Int(x), "y": Int(y), "z": Int(z)}) if kind != "bed": items = [] if (x, y, z) in supplies: item_id, count = supplies[(x, y, z)] items.append(Compound({"Slot": Byte(0), "id": String(item_id), "count": Int(count)})) tag["Items"] = NBTList[Compound](items) if kind in ("furnace", "smoker"): tag.update({"BurnTime": Short(0), "CookTime": Short(0), "CookTimeTotal": Short(200), "RecipesUsed": Compound()}) entities.append(tag) return NBTList[Compound](entities)
#013 · 2026-09-25T15:01:48.958Z · exit=0 · sed -n '161,387p' references/stilt-water-cabin/generate.py
sed -n '161,387p' references/stilt-water-cabin/generate.py

日志保留的输出末尾

t="true", distance="1", waterlogged="false") # Reeds have real dirt substrate and an adjacent source-water cell. for x, z in ((1, 4), (1, 5), (2, 5), (1, 17), (2, 18), (1, 20), (18, 22), (19, 22), (21, 21), (24, 19), (25, 18), (24, 3), (20, 2), (6, 2)): self.put(x, 1, z, "minecraft:dirt") self.put(x, 2, z, "minecraft:mud") for y in range(3, self.rng.choice((5, 5, 6))): self.put(x, y, z, "minecraft:sugar_cane", age="0") for x, z in ((2, 9), (1, 13), (11, 20), (14, 22), (17, 19), (20, 18), (23, 21), (26, 20), (15, 3), (9, 2), (4, 2), (26, 22)): if self.get(x, 2, z) == "minecraft:water[level=0]": self.put(x, 3, z, "minecraft:lily_pad") # Boats are entities in Java 1.21.1. The geometry-only renderer omits them. self.entities.append(Compound({ "id": String("minecraft:boat"), "Type": String("oak"), "Pos": NBTList[Double]([Double(v) for v in BOAT_POSITION]), "Motion": NBTList[Double]([Double(0), Double(0), Double(0)]), "Rotation": NBTList[Float]([Float(90), Float(0)]), "UUID": IntArray([20260923, 1953066084, 1466000227, 1]), "Invulnerable": Byte(0), "OnGround": Byte(0), })) def connect_fences(self) -> None: """Set fence connections explicitly because schematics do not update states.""" for (x, y, z), state in list(self.placed.items()): if state.split("[")[0] not in ("minecraft:spruce_fence", "minecraft:oak_fence"): continue props = {"waterlogged": "false"} for face, dx, dz in (("east", 1, 0), ("west", -1, 0), ("south", 0, 1), ("north", 0, -1)): neighbor = self.get(x + dx, y, z + dz) or "minecraft:air" props[face] = str(any(kind in neighbor for kind in ("_fence", "_log", "_planks"))).lower() self.put(x, y, z, state.split("[")[0], **props)
#014 · 2026-09-25T15:03:15.343Z · exit=0 · mkdir -p hearthwright examples samples test previews && cp generate.py /tmp/hearthwright_template.py
mkdir -p hearthwright examples samples test previews && cp generate.py /tmp/hearthwright_template.py && cp /opt/tools/template_render.mjs render.mjs && /opt/venv/bin/python - <<'PY' import json p=json.load(open('/work/generator/MCRender/resources/1.21.1-blocks.json')) print(type(p),len(p)); print(str(p)[:2200]) PY

日志保留的输出末尾

door': [{'facing': ['north', 'south', 'west', 'east'], 'half': ['upper', 'lower'], 'hinge': ['left', 'right'], 'open': ['true', 'false'], 'powered': ['true', 'false']}, {'facing': 'north', 'half': 'lower', 'hinge': 'left', 'open': 'false', 'powered': 'false'}], 'acacia_fence': [{'east': ['true', 'false'], 'north': ['true', 'false'], 'south': ['true', 'false'], 'waterlogged': ['true', 'false'], 'west': ['true', 'false']}, {'east': 'false', 'north': 'false', 'south': 'false', 'waterlogged': 'false', 'west': 'false'}], 'acacia_fence_gate': [{'facing': ['north', 'south', 'west', 'east'], 'in_wall': ['true', 'false'], 'open': ['true', 'false'], 'powered': ['true', 'false']}, {'facing': 'north', 'in_wall': 'false', 'open': 'false', 'powered': 'false'}], 'acacia_hanging_sign': [{'attached': ['true', 'false'], 'rotation': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'], 'waterlogged': ['true', 'false']}, {'attached': 'false', 'rotation': '0', 'waterlogged': 'false'}], 'acacia_leaves': [{'distance': ['1', '2', '3', '4', '5', '6', '7'], 'persistent': ['true', 'false'], 'waterlogged': ['true', 'false']}, {'distance': '7', 'persistent': 'false', 'waterlogged': 'false'}], 'acacia_log': [{'axis': ['x', 'y', 'z']}, {'axis': 'y'}], 'acacia_planks': [{}, {}], 'acacia_pressure_plate': [{'powered': ['true', 'false']}, {'powered': 'false'}], 'acacia_sapling': [{'stage': ['0', '1']}, {'stage': '0'}], 'acacia_sign': [{'rotation': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'], 'waterlogged': ['true', 'false']}, {'rotation': '0', 'waterlogged': 'false'}], 'acacia_slab': [{'type': ['top', 'bottom', 'double'], 'waterlogged': ['true', 'false']}, {'type': 'bottom', 'waterlogged': 'false'}], 'acacia_stairs': [{'facing': ['north', 'south', 'west', 'east'], 'half': ['top', 'bottom'], 'shape': ['straight', 'inner_left', 'inner_right', 'outer_left', 'outer_right'], 'waterlogged': ['true', 'false']}, {'facing': 'north',
#015 · 2026-09-25T15:07:02.217Z · exit=0 · cat > hearthwright/api.py <<'PY'
cat > hearthwright/api.py <<'PY' # Architectural declarations and bounded sampling domains. from __future__ import annotations from dataclasses import dataclass, field, fields, is_dataclass from typing import Any @dataclass(frozen=True) class Choice: """Declare a finite domain of explicit alternatives. Args: values: Two or more architectural values, or one constrained value. """ values: tuple def __init__(self, *values): if not values: raise ValueError('Choice needs at least one value') object.__setattr__(self, 'values', tuple(values)) @dataclass(frozen=True) class Span: """Declare an inclusive integer domain. Args: low: Minimum value. high: Maximum value. step: Positive increment from low. """ low: int high: int step: int = 1 def __post_init__(self): if self.low > self.high or self.step < 1: raise ValueError('Span requires low <= high and step >= 1') @dataclass(frozen=True) class Materials: """Assign coherent material roles independently of the building composition.""" wall: str = 'stone' frame: str = 'oak' roof: str = 'pale_stone' trim: str = 'spruce' floor: str = 'oak' accent: str = 'green' shutters: bool = True @dataclass(frozen=True) class Roof: """Describe a backed roof surface; ridge direction is relative to the plan.""" form: Any = 'gable' ridge: Any = 'long' pitch: Any = 1.0 eaves: int = 1 dormers: Any = 0 cupola: Any = False @dataclass(frozen=True) class Level: """Allocate domestic functions, partitioning and floor proportions.""" rooms: tuple[str, ...] = ('living', 'kitchen', 'sleeping', 'storage') layout: Any = 'open' height: int = 5 inset: int = 0 wall: str | None = None attic: bool = False @dataclass(frozen=True) class Join: """Connect a new volume to an existing volume by a shared wall and doorway.""" to: str side: Any = 'east' align: Any = 'center' level: int = 0 @dataclass(frozen=True) class Volume: """Compose an enclosed mass with levels, an envelope and vertical circulation.""" name: str width: Any = 11 depth: Any = 11 levels: tuple[Level, ...] = (Level(),) roof: Roof | None = field(default_factory=Roof) join: Join | None = None envelope: str = 'framed' circulation: Any = 'stairs' raised: int = 0 supports: str = 'foundation' root_spread: Any = 4 root_crown: Any = 5 @dataclass(frozen=True) class Attachment: """Attach usable outdoor space at a selected wall and occupied level.""" kind: str to: str side: Any = 'south' level: int = 0 depth: Any = 4 width: Any = 'full' cover: str = 'open' @dataclass(frozen=True) class Feature: """Reserve a site plot with a path to its working edge.""" kind: str side: Any = 'east' scale: Any = 7 @dataclass(frozen=True) class Site: """Define the terrain relationship and planting policy.""" terrain: str = 'meadow' margin: int = 5 planting: str = 'temperate' rise: int = 5 water_depth: int = 2 @dataclass(frozen=True) class Entrance: """Choose a volume, wall, level and bay for the public approach.""" volume: str side: str = 'south' level: int = 0 bay: Any = 'center' @dataclass(frozen=True) class Limits: """Bound generation cost independently of architectural dimensions.""" horizontal: int = 128 vertical: int = 96 volume: int = 1_500_000 blocks: int = 300_000 @dataclass(frozen=True) class Design: """Compose one coherent property without a building-family discriminator.""" name: str volumes: tuple[Volume, ...] materials: Materials = field(default_factory=Materials) site: Site = field(default_factory=Site) entrance: Entrance | None = None attachments: tuple[Attachment, ...] = () features: tuple[Feature, ...] = () limits: Limits = field(default_factory=Limits) class DesignError(ValueError): """Report an unsatisfiable or unsupported architectural constraint.""" def domain(value): """Expand a single bounded parameter into its declared alternatives.""" if isinstance(value, Choice): return value.values if isinstance(value, Span): return tuple(range(value.low, value.high + 1, value.step)) return (value,) def choose(value, rng, allowed=None, context='parameter'): """Sample only feasible declared choices; reject contradictory intent.""" candidates = [v for v in domain(value) if allowed is None or allowed(v)] if not candidates: raise DesignError(f'{context}: no feasible value in {domain(value)!r}') return candidates[rng.randrange(len(candidates))] if len(candidates) > 1 else candidates[0] def document(value): """Convert a declaration to a stable, inspectable JSON structure.""" if is_dataclass(value): return {'type': type(value).__name__, **{f.name: document(getattr(value, f.name)) for f in fields(value)}} if isinstance(value, (tuple, list)): return [document(v) for v in value] if isinstance(value, dict): return {str(k): document(v) for k, v in value.items()} return value PY cat > hearthwright/materials.py <<'PY' # Harmonious material-role vocabularies shared by every composition. WALLS = { 'stone': ('andesite', 'andesite', 'stone', 'cobblestone'), 'masonry': ('stone_bricks', 'stone_bricks', 'andesite', 'mossy_stone_bricks'), 'plaster': ('calcite', 'calcite', 'smooth_sandstone', 'white_terracotta'), 'white': ('smooth_quartz', 'smooth_quartz', 'calcite'), 'blue': ('light_blue_concrete', 'light_blue_concrete', 'light_blue_terracotta'), 'timber': ('oak_planks', 'oak_planks', 'stripped_oak_wood'), 'earth': ('tuff', 'mossy_cobblestone', 'andesite'), } # Full backing, stair, slab: real vanilla block names, never synthesized stone plurals. ROOFS = { 'pale_stone': ('andesite', 'andesite_stairs', 'andesite_slab'), 'sandstone': ('smooth_sandstone', 'smooth_sandstone_stairs', 'smooth_sandstone_slab'), 'slate': ('deepslate_tiles', 'deepslate_tile_stairs', 'deepslate_tile_slab'), 'gray': ('stone_bricks', 'stone_brick_stairs', 'stone_brick_slab'), 'red': ('mangrove_planks', 'mangrove_stairs', 'mangrove_slab'), 'tile': ('bricks', 'brick_stairs', 'brick_slab'), 'dark_wood': ('dark_oak_planks', 'dark_oak_stairs', 'dark_oak_slab'), } WOODS = ('oak', 'spruce', 'dark_oak', 'birch', 'jungle', 'acacia', 'mangrove', 'cherry', 'warped') COLORS = ('white', 'light_gray', 'gray', 'black', 'brown', 'red', 'orange', 'yellow', 'lime', 'green', 'cyan', 'light_blue', 'blue', 'purple', 'magenta', 'pink') FUNCTIONS = ('living', 'kitchen', 'sleeping', 'storage', 'workshop', 'study', 'enchanting', 'brewing', 'trading', 'fishing') DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)} OPPOSITE = {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'} def log(wood): """Resolve the timber role including nether stems.""" return 'stripped_warped_stem' if wood == 'warped' else f'stripped_{wood}_log' PY cat > hearthwright/plan.py <<'PY' # Resolve relationships, constrained domains, room masks and site reservations. from __future__ import annotations from dataclasses import asdict, dataclass, field import hashlib import json import random from .api import Design, DesignError, choose, document, domain from .materials import WALLS, ROOFS, WOODS, COLORS, FUNCTIONS, DIRECTIONS @dataclass class Rect: """Hold inclusive horizontal bounds, independent of schematic coordinates.""" x0: int z0: int x1: int z1: int @property def width(self): return self.x1 - self.x0 + 1 @property def depth(self): return self.z1 - self.z0 + 1 @property def center(self): return ((self.x0 + self.x1) // 2, (self.z0 + self.z1) // 2) def inset(self, n): return Rect(self.x0 + n, self.z0 + n, self.x1 - n, self.z1 - n) def cells(self): return {(x, z) for x in range(self.x0, self.x1 + 1) for z in range(self.z0, self.z1 + 1)} def contains(self, x, z): return self.x0 <= x <= self.x1 and self.z0 <= z <= self.z1 @dataclass class Storey: """Record a resolved floor and its buildable room program.""" id: str rect: Rect floor: int height: int functions: tuple layout: str wall: str attic: bool cells: set = field(default_factory=set) @dataclass class Mass: """Record a connected envelope with a roof and support requirements.""" name: str rect: Rect levels: list[Storey] roof: dict | None envelope: str circulation: str supports: str roots: dict join: dict | None = None @dataclass class Plan: """Retain sampled decisions and spatial relationships for reproduction.""" design: Design seed: int masses: list[Mass] attachments: list[dict] features: list[dict] entrance: dict site: Rect def summary(self): masses = [] for m in self.masses: v = asdict(m) for level in v['levels']: level['cells'] = sorted(level['cells']) masses.append(v) return {'schema': 1, 'seed': self.seed, 'design': document(self.design), 'masses': masses, 'attachments': [{**a, 'rect': asdict(a['rect'])} for a in self.attachments], 'features': [{**a, 'rect': asdict(a['rect'])} for a in self.features], 'entrance': self.entrance, 'site': asdict(self.site)} def signature(self): """Hash geometry and room decisions, excluding labels, materials and planting.""" payload = [] for m in self.masses: payload.append([asdict(m.rect), [(asdict(s.rect), s.floor, s.height, s.layout, s.functions, s.attic) for s in m.levels], m.roof, m.envelope, m.circulation, m.supports, m.roots if m.envelope == 'roots' else None]) payload += [[a['kind'], asdict(a['rect']), a['floor'], a['cover']] for a in self.attachments] payload += [[a['kind'], asdict(a['rect'])] for a in self.features] payload.append([self.entrance['side'], self.entrance['floor'], self.entrance['point']]) return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest() def edge_point(rect, side, bay='center'): """Return a facade point selected by a relative bay.""" fraction = {'left': .3, 'center': .5, 'right': .7}[bay] if side in ('south', 'north'): return (rect.x0 + round((rect.width - 1) * fraction), rect.z1 if side == 'south' else rect.z0) return (rect.x1 if side == 'east' else rect.x0, rect.z0 + round((rect.depth - 1) * fraction)) def outline(rect, organic=False): """Chamfer organic floor corners, retaining ample domestic room area.""" cells = rect.cells() if organic: cells = {(x, z) for x, z in cells if min(x - rect.x0, rect.x1 - x) + min(z - rect.z0, rect.z1 - z) >= 2} return cells def resolve(design: Design, seed: int) -> Plan: """Compile a declaration into compatible, bounded spatial decisions. Args: design: Public composition. seed: Any signed 64-bit integer. Returns: Plan: Resolved architecture, independent of block placement. """ if type(seed) is not int or not -(2**63) <= seed < 2**63: raise DesignError('seed must be a signed 64-bit integer') if not design.volumes: raise DesignError('a dwelling needs at least one volume') rng = random.Random(seed) mat = design.materials for value, collection, role in ((mat.wall, WALLS, 'wall'), (mat.roof, ROOFS, 'roof'), (mat.frame, WOODS, 'frame'), (mat.trim, (*WOODS, 'sandstone'), 'trim'), (mat.floor, WOODS, 'floor'), (mat.accent, COLORS, 'accent')): if value not in collection: raise DesignError(f'unknown {role} role {value!r}') if design.site.terrain not in ('meadow', 'slope', 'water', 'hill'): raise DesignError('terrain must be meadow, slope, water or hill') if not 3 <= design.site.margin <= 12 or not 1 <= design.site.water_depth <= 3 or not 2 <= design.site.rise <= 7: raise DesignError('site margin 3..12, water_depth 1..3, rise 2..7 required') if design.site.planting not in ('temperate', 'mediterranean', 'conifer', 'wetland', 'sparse'): raise DesignError('unknown planting policy') masses, by_name = [], {} for v in design.volumes: if v.name in by_name: raise DesignError(f'duplicate volume {v.name!r}') if not 1 <= len(v.levels) <= 4: raise DesignError(f'{v.name}: provide 1..4 occupied levels') if v.envelope not in ('framed', 'roots') or v.supports not in ('foundation', 'posts'): raise DesignError(f'{v.name}: unknown envelope or supports') if v.envelope == 'roots' and (v.roof is not None or len(v.levels) > 2): raise DesignError(f'{v.name}: root envelopes need roof=None and at most two levels') if v.envelope != 'roots' and v.roof is None: raise DesignError(f'{v.name}: framed envelopes require a Roof') if not 0 <= v.raised <= 7: raise DesignError(f'{v.name}: raised must be 0..7') if design.site.terrain == 'water' and (v.supports != 'posts' or v.raised < design.site.water_depth + 1): raise DesignError(f'{v.name}: water buildings need posts and raised >= water_depth + 1') max_inset = max(s.inset for s in v.levels) min_width = 9 + max_inset * 2 min_depth = 9 + max_inset * 2 stairs_required = len(v.levels) > 1 and domain(v.circulation) == ('stairs',) if stairs_required: min_depth = max(min_depth, max(s.height for s in v.levels[:-1]) + 6 + max_inset * 2) min_width = max(min_width, 11 + max_inset * 2) w = choose(v.width, rng, lambda n: type(n) is int and min_width <= n <= 25, f'{v.name}.width (minimum {min_width}, maximum 25)') d = choose(v.depth, rng, lambda n: type(n) is int and min_depth <= n <= 23, f'{v.name}.depth (minimum {min_depth}, maximum 23)') rect = Rect(0, 0, w - 1, d - 1) floor = 2 + v.raised join = None if v.join: j = v.join if j.to not in by_name: raise DesignError(f'{v.name}: join target must be an earlier volume') parent = by_name[j.to] if not 0 <= j.level < len(parent.levels): raise DesignError(f'{v.name}: join level does not exist') if v.raised: raise DesignError(f'{v.name}: joined floor inherits its target level; omit raised') target = parent.levels[j.level] side = choose(j.side, rng, lambda s: s in DIRECTIONS, f'{v.name}.join.side') align = choose(j.align, rng, lambda s: s in ('front', 'center', 'back'), f'{v.name}.join.align') fraction = {'front': 0, 'center': .5, 'back': 1}[align] p = target.rect inset = v.levels[0].inset if side in ('east', 'west'): z0 = p.z0 + round((p.depth - (d - 2 * inset)) * fraction) - inset x0 = p.x1 - inset if side == 'east' else p.x0 - w + 1 + inset else: x0 = p.x0 + round((p.width - (w - 2 * inset)) * fraction) - inset z0 = p.z1 - inset if side == 'south' else p.z0 - d + 1 + inset rect = Rect(x0, z0, x0 + w - 1, z0 + d - 1) floor = target.floor q = rect.inset(inset) if side in ('east', 'west'): point = (p.x1 if side == 'east' else p.x0, (max(p.z0, q.z0) + min(p.z1, q.z1)) // 2) else: point = ((max(p.x0, q.x0) + min(p.x1, q.x1)) // 2, p.z1 if side == 'south' else p.z0) join = {'to': j.to, 'level': j.level, 'side': side, 'point': point, 'floor': floor} elif masses: raise DesignError(f'{v.name}: every additional volume needs a Join') levels = [] for i, s in enumerate(v.levels): if not 0 <= s.inset <= 3 or s.wall is not None and s.wall not in WALLS: raise DesignError(f'{v.name}/{i}: inset must be 0..3 and wall a material role') if not 5 <= s.height <= 7: raise DesignError(f'{v.name}/{i}: storey height must be 5..7') if s.attic and (i != len(v.levels) - 1 or v.roof is None): raise DesignError(f'{v.name}/{i}: attic must be the last level under a roof') if not s.rooms or any(r not in FUNCTIONS for r in s.rooms): raise DesignError(f'{v.name}/{i}: invalid room function') r = rect.inset(s.inset) options = ('open',) if v.envelope == 'roots' else tuple(['open'] + (['cross'] if r.width >= 15 and len(s.rooms) > 1 else []) + (['long'] if r.depth >= 15 and len(s.rooms) > 1 else [])) layout = choose(s.layout, rng, lambda p: p in options, f'{v.name}/{i}.layout; feasible {options}') height = 3 if s.attic else s.height levels.append(Storey(f'{v.name}/{i}', r, floor, height, s.rooms, layout, s.wall or mat.wall, s.attic, outline(r, v.envelope == 'roots'))) floor += s.height can_stair = all(min(a.rect.depth, b.rect.depth) >= a.height + 6 and min(a.rect.width, b.rect.width) >= 11 for a, b in zip(levels, levels[1:])) circulation = choose(v.circulation, rng, lambda c: c == 'ladder' or c == 'stairs' and can_stair, f'{v.name}.circulation') roof = None if v.roof: r = v.roof form = choose(r.form, rng, lambda t: t in ('gable', 'hip'), f'{v.name}.roof.form') ridge = choose(r.ridge, rng, lambda t: t in ('x', 'z', 'long', 'short'), f'{v.name}.roof.ridge') if ridge in ('long', 'short'): ridge = ('x' if w >= d else 'z') if ridge == 'long' else ('z' if w >= d else 'x') pitch = choose(r.pitch, rng, lambda n: n in (.5, 1.0, 1.5) and (not levels[-1].attic or n >= 1), f'{v.name}.roof.pitch') if r.eaves not in (1, 2): raise DesignError('roof eaves must be 1 or 2') length = levels[-1].rect.width if ridge == 'x' else levels[-1].rect.depth dormers = choose(r.dormers, rng, lambda n: type(n) is int and 0 <= n <= (2 if length >= 17 else 1) and (n == 0 or form == 'gable' and pitch >= 1), f'{v.name}.roof.dormers') cupola = choose(r.cupola, rng, lambda b: type(b) is bool, f'{v.name}.roof.cupola') roof = dict(form=form, ridge=ridge, pitch=pitch, eaves=r.eaves, dormers=dormers, cupola=cupola) roots = dict(spread=choose(v.root_spread, rng, lambda n: type(n) is int and 3 <= n <= 6, 'root_spread'), crown=choose(v.root_crown, rng, lambda n: type(n) is int and 3 <= n <= 7, 'root_crown')) mass = Mass(v.name, rect, levels, roof, v.envelope, circulation, v.supports, roots, join) # Shared boundary is legal; overlapping occupied interiors are not. for other in masses: for a in mass.levels: for b in other.levels: if max(a.floor, b.floor) < min(a.floor + a.height + 1, b.floor + b.height + 1): if a.rect.inset(1).cells() & b.rect.inset(1).cells(): raise DesignError(f'{v.name} intersects occupied space in {other.name}; change join side/alignment or insets') masses.append(mass) by_name[v.name] = mass entry = design.entrance main = by_name.get(entry.volume if entry else masses[0].name) if main is None or entry and not 0 <= entry.level < len(main.levels): raise DesignError('entrance volume/level does not exist') side = entry.side if entry else 'south' if side not in DIRECTIONS: raise DesignError('entrance side must be a cardinal direction') level = main.levels[entry.level if entry else 0] bay = choose(entry.bay if entry else 'center', rng, lambda b: b in ('left', 'center', 'right'), 'entrance.bay') entrance = dict(volume=main.name, level=entry.level if entry else 0, side=side, point=edge_point(level.rect, side, bay), floor=level.floor) attachments = [] occupied = [m.rect.inset(-2) for m in masses] for index, a in enumerate(design.attachments): if a.to not in by_name or not 0 <= a.level < len(by_name[a.to].levels): raise DesignError('attachment target/level does not exist') if a.kind not in ('porch', 'deck', 'terrace', 'balcony', 'market') or a.cover not in ('open', 'pergola', 'canopy'): raise DesignError('invalid attachment kind or cover') s = by_name[a.to].levels[a.level] side_a = choose(a.side, rng, lambda t: t in DIRECTIONS, 'attachment.side') depth = choose(a.depth, rng, lambda n: type(n) is int and 3 <= n <= 7, 'attachment.depth') span = s.rect.width if side_a in ('north', 'south') else s.rect.depth width = choose(a.width, rng, lambda n: n == 'full' or type(n) is int and 5 <= n <= span, 'attachment.width') width = span if width == 'full' else width cx, cz = edge_point(s.rect, side_a) if side_a in ('north', 'south'): x0 = cx - width // 2 z0 = cz + 1 if side_a == 'south' else cz - depth ar = Rect(x0, z0, x0 + width - 1, z0 + depth - 1) else: z0 = cz - width // 2 x0 = cx + 1 if side_a == 'east' else cx - depth ar = Rect(x0, z0, x0 + depth - 1, z0 + width - 1) for m in masses: for l in m.levels: if l.floor <= s.floor < l.floor + l.height and ar.cells() & l.rect.cells(): raise DesignError(f'attachment {index} overlaps {m.name}; use a free facade') for previous in attachments: if abs(previous['floor'] - s.floor) < 4 and ar.cells() & previous['rect'].cells(): raise DesignError(f'attachment {index} overlaps another outdoor floor') attachments.append(dict(id=f'attachment/{index}', kind=a.kind, to=a.to, level=a.level, side=side_a, rect=ar, floor=s.floor, cover=a.cover, door=(cx, cz))) occupied.append(ar) # Lay site plots beside the complete massing, never at reference-specific coordinates. minx = min(r.x0 for r in occupied) maxx = max(r.x1 for r in occupied) minz = min(r.z0 for r in occupied) maxz = max(r.z1 for r in occupied) features = [] cursors = dict(east=minz, west=minz, north=minx, south=minx) for index, f in enumerate(design.features): if f.kind not in ('pool', 'field', 'well', 'windmill', 'landing', 'garden'): raise DesignError(f'unknown site component {f.kind!r}') side_f = choose(f.side, rng, lambda t: t in DIRECTIONS, 'feature.side') size = choose(f.scale, rng, lambda n: type(n) is int and 5 <= n <= 11, 'feature.scale') if f.kind == 'windmill': size = max(9, size) start = cursors[side_f] if side_f in ('east', 'west'): x0 = maxx + 4 if side_f == 'east' else minx - 3 - size z0 = start else: x0 = start z0 = maxz + 4 if side_f == 'south' else minz - 3 - size fr = Rect(x0, z0, x0 + size - 1, z0 + size - 1) cursors[side_f] += size + 3 features.append(dict(id=f'feature/{index}', kind=f.kind, side=side_f, rect=fr)) occupied.append(fr) # Approach length includes every rise and a generous dry landing. dx, dz = DIRECTIONS[side] ex, ez = entrance['point'] length = entrance['floor'] + 10 occupied.append(Rect(ex + dx * length - 2, ez + dz * length - 2, ex + dx * length + 2, ez + dz * length + 2)) extra = max([m.roots['spread'] + 3 for m in masses if m.envelope == 'roots'] + [design.site.margin]) site = Rect(min(r.x0 for r in occupied) - extra, min(r.z0 for r in occupied) - extra, max(r.x1 for r in occupied) + extra, max(r.z1 for r in occupied) + extra) if max(site.width, site.depth) + 2 > design.limits.horizontal: raise DesignError('site exceeds horizontal resource limit; reduce components or increase Limits') plan = Plan(design, seed, masses, attachments, features, entrance, site) return plan PY

日志保留的输出末尾

#016 · 2026-09-25T15:08:54.198Z · exit=0 · cat > hearthwright/blocks.py <<'PY'
cat > hearthwright/blocks.py <<'PY' # Sparse block placement, authoritative states and deterministic Litematic transport. from __future__ import annotations from functools import lru_cache import json import os from pathlib import Path import random import sys MCIO_ROOT = Path(os.environ.get('MCIO_ROOT', '/work/generator/MCIO')) STATE_FILE = Path(os.environ.get('MINECRAFT_STATES', '/work/generator/MCRender/resources/1.21.1-blocks.json')) sys.path.insert(0, str(MCIO_ROOT)) from mcio.sketch import LitematicCanvas, parse_blockstate from mcio.schematic import load_schematic from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Compound, Int, String, Byte, Short, List as NBTList AIR = 'minecraft:air' @lru_cache(maxsize=1) def registry(): """Load vanilla 1.21.1 property domains and defaults from the installed authority.""" return json.loads(STATE_FILE.read_text()) @lru_cache(maxsize=None) def split_state(state): """Parse a blockstate while retaining its explicit property set.""" parsed = parse_blockstate(state).state.unpack() return parsed['Name'], parsed.get('Properties', {}) def blockstates_equivalent(left: str, right: str) -> bool: """Compare names and explicit properties, ignoring property insertion order.""" return parse_blockstate(left).state.unpack() == parse_blockstate(right).state.unpack() @lru_cache(maxsize=None) def make_state(block, pairs=()): """Fill generated states with authoritative defaults and reject invalid values.""" if '[' in block: name, props = split_state(block) return make_state(name, tuple(sorted(props.items()))) short = block.removeprefix('minecraft:') if short not in registry(): raise ValueError(f'unknown Java 1.21.1 block {block}') domains, defaults = registry()[short] props = dict(defaults) for key, value in pairs: if key not in domains or str(value) not in domains[key]: raise ValueError(f'invalid state {short}.{key}={value!r}') props[key] = str(value) return 'minecraft:' + short + ('[' + ','.join(f'{k}={v}' for k, v in sorted(props.items())) + ']' if props else '') def name(state): """Read a short block name without parsing all properties.""" return state.split('[', 1)[0].removeprefix('minecraft:') def is_air(state): return name(state) in ('air', 'cave_air', 'void_air') def passable(state): """Conservative player clearance for the vocabulary emitted by this library.""" n = name(state) return (is_air(state) or n.endswith('_door') and not n.endswith('iron_door') or n.endswith('_carpet') or n in ('ladder', 'short_grass', 'fern', 'snow') or n.endswith('_fence_gate') and split_state(state)[1].get('open') == 'true') def full_support(state): """Identify blocks with a usable full top, excluding decorative partial blocks.""" n = name(state) if is_air(state) or n in ('water', 'lava', 'ladder', 'short_grass', 'fern', 'lily_pad', 'seagrass'): return False if any(n.endswith(suffix) for suffix in ('_door', '_trapdoor', '_fence', '_fence_gate', '_wall', '_pane', '_carpet', '_bed', '_leaves', '_stairs')): return False if n.endswith('_slab'): return split_state(state)[1].get('type') in ('top', 'double') if n.startswith('potted_') or n in ('lantern', 'chain', 'flower_pot', 'torch', 'wall_torch', 'brewing_stand', 'wheat', 'poppy', 'cornflower', 'allium', 'oxeye_daisy', 'dandelion', 'sugar_cane') or 'candle' in n: return False return True def floor_support(state): return full_support(state) or name(state).endswith('_stairs') and split_state(state)[1].get('half', 'bottom') == 'bottom' def seal(state): """Require full backing for partial roof blocks; closed doors are weather seals.""" n = name(state) return full_support(state) or n.endswith('_door') and split_state(state)[1].get('open') == 'false' class Grid: """Store world coordinates sparsely and derive export bounds from final geometry.""" def __init__(self, seed=0): self.blocks = {} self.rng = random.Random(seed) self.reservations = {} self.metadata = {'schema': 1, 'rooms': [], 'routes': [], 'openings': [], 'supports': [], 'furniture': [], 'lights': [], 'interior': [], 'features': [], 'stairs': []} self.inventories = {} def get(self, x, y, z): return self.blocks.get((x, y, z), AIR) def put(self, x, y, z, block, **props): if any(type(v) is not int for v in (x, y, z)): raise ValueError(f'integer coordinates required: {(x, y, z)}') state = make_state(block, tuple(sorted(props.items()))) if is_air(state): self.blocks.pop((x, y, z), None) self.inventories.pop((x, y, z), None) else: self.blocks[x, y, z] = state def box(self, bounds, block, **props): x0, y0, z0, x1, y1, z1 = bounds if x1 < x0 or y1 < y0 or z1 < z0: return for x in range(x0, x1 + 1): for y in range(y0, y1 + 1): for z in range(z0, z1 + 1): self.put(x, y, z, block, **props) def texture(self, bounds, palette): x0, y0, z0, x1, y1, z1 = bounds for x in range(x0, x1 + 1): for y in range(y0, y1 + 1): for z in range(z0, z1 + 1): self.put(x, y, z, self.rng.choice(palette)) def reserve(self, cells, owner, purpose='passage'): for p in cells: self.reservations.setdefault(tuple(p), []).append((owner, purpose)) def route(self, points, owner, headroom=2): points = [tuple(p) for p in points] self.metadata['routes'].append({'owner': owner, 'points': points, 'headroom': headroom}) self.reserve([(x, y + dy, z) for x, y, z in points for dy in range(headroom)], owner) def free(self, cells): return all(tuple(p) not in self.reservations and is_air(self.get(*p)) for p in cells) @property def bounds(self): if not self.blocks: return (0, 0, 0, 0, 0, 0) axes = list(zip(*self.blocks)) return tuple(min(a) for a in axes) + tuple(max(a) for a in axes) def stair(self, x, y, z, material='spruce', facing='south', half='bottom'): self.put(x, y, z, material + '_stairs', facing=facing, half=half, shape='straight') def door(self, x, floor, z, side, owner, wood='spruce'): for dy, half in ((1, 'lower'), (2, 'upper')): self.put(x, floor + dy, z, wood + '_door', facing=side, half=half, hinge='left', open='false') self.metadata['openings'].append({'kind': 'door', 'owner': owner, 'cells': [(x, floor + 1, z), (x, floor + 2, z)], 'side': side}) def lantern(self, x, y, z, owner, hanging=False): self.put(x, y, z, 'lantern', hanging=str(hanging).lower()) self.metadata['lights'].append({'owner': owner, 'point': (x, y, z)}) def inventory(self, p, items): self.inventories[tuple(p)] = items def export(self, path: Path, title='Hearthwright'): """Write one region and an explicit world-coordinate design sidecar. Args: path: Destination .litematic path. title: Schematic metadata title. Returns: Path: Written file. """ path = Path(path) lo_x, lo_y, lo_z, hi_x, hi_y, hi_z = self.bounds origin = (lo_x - 1, min(0, lo_y), lo_z - 1) sx, sy, sz = hi_x - origin[0] + 2, hi_y - origin[1] + 2, hi_z - origin[2] + 2 canvas = LitematicCanvas((sy, sz, sx)) tiles = [] for (x, y, z), state in sorted(self.blocks.items()): px, py, pz = x - origin[0], y - origin[1], z - origin[2] canvas.block((py, pz, px), parse_blockstate(state)) n = name(state) kind = 'bed' if n.endswith('_bed') else n if kind in ('bed', 'barrel', 'chest', 'smoker', 'furnace'): tag = Compound({'id': String('minecraft:' + kind), 'x': Int(px), 'y': Int(py), 'z': Int(pz)}) if kind != 'bed': tag['Items'] = NBTList[Compound]([Compound({'Slot': Byte(i), 'id': String('minecraft:' + item), 'count': Int(count)}) for i, (item, count) in enumerate(self.inventories.get((x, y, z), []))]) if kind in ('smoker', 'furnace'): tag.update({'BurnTime': Short(0), 'CookTime': Short(0), 'CookTimeTotal': Short(200), 'RecipesUsed': Compound()}) tiles.append(tag) schematic = canvas.to_litematica(name=title, author='Hearthwright', description='Composed architecture; design and validation metadata in adjacent JSON.', minecraft_data_version=3955) schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 nbt = schematic.write_to_nbt() region = next(iter(nbt['Regions'].values())) region['TileEntities'] = NBTList[Compound](tiles) path.parent.mkdir(parents=True, exist_ok=True) NBTFile(nbt).save(path) self.metadata['origin'] = origin self.metadata['size_xyz'] = (sx, sy, sz) self.metadata['inventories'] = [{'point': p, 'items': items} for p, items in sorted(self.inventories.items())] path.with_suffix('.json').write_text(json.dumps(self.metadata, indent=2, sort_keys=True) + '\n') return path def verify(self, path: Path): """Compare the entire reloaded non-air volume semantically, including extra cells.""" other = Grid.load(path) if set(self.blocks) != set(other.blocks): raise AssertionError('non-air coordinate set changed on reload') for p, state in self.blocks.items(): if not blockstates_equivalent(state, other.blocks[p]): raise AssertionError(f'cell {p} changed on reload: {state!r} -> {other.blocks[p]!r}') return {'blocks': len(self.blocks), 'states': len(set(self.blocks.values())), 'size_xyz': other.metadata['size_xyz']} @classmethod def load(cls, path: Path, metadata: dict | Path | None = None): """Reload geometry with explicit metadata, defaulting to the adjacent sidecar.""" path = Path(path) if metadata is None: metadata = path.with_suffix('.json') if isinstance(metadata, (str, Path)): metadata = json.loads(Path(metadata).read_text()) grid = cls() grid.metadata = metadata ox, oy, oz = metadata['origin'] schematic = load_schematic(path) array = schematic.read_flat(0, schematic.volume).reshape(schematic.size_yzx) for y, z, x in zip(*array.nonzero()): # Palette index zero is usually air, but not guaranteed by other writers. pass for index, state in enumerate(schematic.palette): if is_air(state): continue import numpy as np for y, z, x in zip(*np.where(array == index)): grid.blocks[int(x) + ox, int(y) + oy, int(z) + oz] = state for route in metadata['routes']: grid.reserve([(x, y + dy, z) for x, y, z in route['points'] for dy in range(route['headroom'])], route['owner']) grid.inventories = {tuple(record['point']): record['items'] for record in metadata.get('inventories', [])} return grid PY

日志保留的输出末尾

#017 · 2026-09-25T15:11:57.602Z · exit=0 · cat > hearthwright/navigation.py <<'PY'
cat > hearthwright/navigation.py <<'PY' # Conservative voxel navigation with directional stair transitions and supported ladders. from collections import deque from .blocks import passable, floor_support, name, split_state from .materials import DIRECTIONS def walkable(grid, p): """Test actual foot/head cells and the supporting floor.""" x, y, z = p return passable(grid.get(x, y, z)) and passable(grid.get(x, y + 1, z)) and (floor_support(grid.get(x, y - 1, z)) or name(grid.get(x, y, z)) == 'ladder') def neighbors(grid, p): """Yield traversable neighbors, allowing rises only on correctly facing stairs.""" x, y, z = p for direction, (dx, dz) in DIRECTIONS.items(): for dy in (0, 1, -1): q = (x + dx, y + dy, z + dz) if not walkable(grid, q): continue if dy: # Stair facing is the direction from the low end to the high end. high = q if dy == 1 else p block = grid.get(high[0], high[1] - 1, high[2]) expected = direction if dy == 1 else {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'}[direction] if not name(block).endswith('_stairs') or split_state(block)[1].get('facing') != expected: # Leaving a ladder onto a landing is also a legal transition. if name(grid.get(*p)) != 'ladder' and name(grid.get(*q)) != 'ladder': continue # A rising player's head sweeps the higher source cell. if not passable(grid.get(x if dy == 1 else q[0], max(y, q[1]) + 1, z if dy == 1 else q[2])): continue yield q if name(grid.get(*p)) == 'ladder': for dy in (-1, 1): q = x, y + dy, z if name(grid.get(*q)) == 'ladder' and walkable(grid, q): yield q def flood(grid, start, allowed=None, maximum=200000): """Return reachable positions and a predecessor map from actual geometry.""" start = tuple(start) if not walkable(grid, start): return {} parents = {start: None} queue = deque([start]) while queue: p = queue.popleft() for q in neighbors(grid, p): if q in parents or allowed is not None and q not in allowed: continue parents[q] = p queue.append(q) if len(parents) > maximum: raise ValueError('navigation resource limit exceeded') return parents def path_to(parents, target): """Recover one deterministic path from a reachable target.""" p = tuple(target) result = [] while p is not None: result.append(p) p = parents[p] return list(reversed(result)) PY cat > hearthwright/roofs.py <<'PY' # Backed roof unions, dormer height fields and structural root sweeps. import math from .materials import ROOFS, WALLS, DIRECTIONS, log from .blocks import is_air def surface(mass): """Resolve a gable or hip to a sealed height field with integrated dormer bumps.""" s = mass.levels[-1] r = mass.roof rect = s.rect.inset(-r['eaves']) base = s.floor + s.height result = {} dormer_centers = [] if r['dormers']: lo, hi = (s.rect.x0, s.rect.x1) if r['ridge'] == 'x' else (s.rect.z0, s.rect.z1) dormer_centers = [lo + round((hi - lo) * f) for f in ([.5] if r['dormers'] == 1 else [.27, .73])] for x, z in sorted(rect.cells()): dx = min(x - rect.x0, rect.x1 - x) dz = min(z - rect.z0, rect.z1 - z) cross = dz if r['ridge'] == 'x' else dx run = min(dx, dz) if r['form'] == 'hip' else cross h = base + math.floor(run * r['pitch']) face = ('south' if z < rect.center[1] else 'north') if r['ridge'] == 'x' else ('east' if x < rect.center[0] else 'west') verge = (x in (rect.x0, rect.x1)) if r['ridge'] == 'x' else (z in (rect.z0, rect.z1)) if r['form'] == 'hip' and dx < dz: face = 'east' if x < rect.center[0] else 'west' if r['form'] == 'hip' and dz <= dx: face = 'south' if z < rect.center[1] else 'north' for center in dormer_centers: lateral = x if r['ridge'] == 'x' else z along = z if r['ridge'] == 'x' else x front = s.rect.z1 if r['ridge'] == 'x' else s.rect.x0 into = front - along if r['ridge'] == 'x' else along - front if abs(lateral - center) <= 2 and -1 <= into <= 4: dh = base + 5 - abs(lateral - center) if dh > h: h = dh face = ('east' if x < center else 'west') if r['ridge'] == 'x' else ('south' if z < center else 'north') verge = into == -1 result[x, z] = dict(height=h, face=face, trim=verge, owner=mass.name) return result, dormer_centers def build_roofs(ctx): """Merge roof surfaces before placement, then close gables and install crowns.""" g, plan, mat = ctx.g, ctx.plan, ctx.plan.design.materials maps, centers = {}, {} merged = {} for mass in plan.masses: if mass.roof is None: continue maps[mass.name], centers[mass.name] = surface(mass) for p, value in maps[mass.name].items(): if p not in merged or value['height'] > merged[p]['height']: merged[p] = value for mass in plan.masses: if mass.roof is None: continue s = mass.levels[-1] heights = maps[mass.name] inner = ctx.inner[s.id] for x, z in sorted(s.cells): h = heights[x, z]['height'] if (x, z) not in inner: for y in range(s.floor + s.height, h): g.put(x, y, z, log(mat.frame) if x == s.rect.center[0] or z == s.rect.center[1] else g.rng.choice(WALLS[s.wall]), **({'axis': 'y'} if x == s.rect.center[0] or z == s.rect.center[1] else {})) elif s.attic: g.box((x, s.floor + 1, z, x, h - 1, z), 'air') ctx.interior.update((x, y, z) for y in range(s.floor + 1, h)) # Dormer glazing sits in the actual continuous gable wall. for center in centers[mass.name]: if mass.roof['ridge'] == 'x': points = [(center + d, s.floor + s.height + y, s.rect.z1) for d in (-1, 0, 1) for y in (1, 2)] side = 'south' else: points = [(s.rect.x0, s.floor + s.height + y, center + d) for d in (-1, 0, 1) for y in (1, 2)] side = 'west' for p in points: g.put(*p, 'light_gray_stained_glass') g.metadata['openings'].append({'kind': 'window', 'owner': s.id, 'cells': points, 'side': side}) backing, stair, slab = ROOFS[mat.roof] trim = ('smooth_sandstone', 'smooth_sandstone_stairs', 'smooth_sandstone_slab') if mat.trim == 'sandstone' else (mat.trim + '_planks', mat.trim + '_stairs', mat.trim + '_slab') for (x, z), v in sorted(merged.items()): h = v['height'] # Eaves stop against taller inhabited volumes, sealing at their solid walls. if any(m.name != v['owner'] and any((x, z) in ctx.inner[s.id] and s.floor <= h + 1 <= s.floor + s.height for s in m.levels) for m in plan.masses): continue b, st, sl = trim if v['trim'] else (backing, stair, slab) neighbor = [merged[x + dx, z + dz]['height'] for dx, dz in DIRECTIONS.values() if (x + dx, z + dz) in merged] low = max(h - 2, min(neighbor, default=h)) # Solid vertical risers eliminate cracks even for the 3:2 profile. for y in range(low, h + 1): if (x, y, z) not in ctx.interior: g.put(x, y, z, b) g.put(x, h, z, b) # Ridge caps use slabs. Other courses preserve a clear tile rhythm. if all(h >= v2 for v2 in neighbor) and sum(h == v2 for v2 in neighbor) >= 1: g.put(x, h + 1, z, sl, type='bottom') else: g.put(x, h + 1, z, st, facing=v['face'], half='bottom', shape='straight') ctx.roof_maps = maps for mass in plan.masses: if mass.roof and mass.roof['cupola']: cupola(ctx, mass, maps[mass.name]) def cupola(ctx, mass, heights): """Build a sealed ridge lantern with an explicit non-occupied solid core.""" g, mat = ctx.g, ctx.plan.design.materials cx, cz = mass.levels[-1].rect.center base = heights[cx, cz]['height'] + 1 roofmat, stairs, slab = ROOFS[mat.roof] for x in range(cx - 2, cx + 3): for z in range(cz - 2, cz + 3): bottom = heights[x, z]['height'] g.box((x, bottom, z, x, base, z), mat.floor + '_planks') for y in range(base + 1, base + 4): edge = x in (cx - 2, cx + 2) or z in (cz - 2, cz + 2) corner = x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) g.put(x, y, z, log(mat.frame) if corner else 'glass' if edge else 'glowstone', **({'axis': 'y'} if corner else {})) for radius in (3, 2, 1, 0): y = base + 7 - radius for x in range(cx - radius, cx + radius + 1): for z in range(cz - radius, cz + radius + 1): g.put(x, y, z, roofmat) if radius and (x in (cx - radius, cx + radius) or z in (cz - radius, cz + radius)): face = 'east' if x == cx - radius else 'west' if x == cx + radius else 'south' if z == cz - radius else 'north' g.put(x, y + 1, z, stairs, facing=face) g.put(cx, base + 8, cz, 'lightning_rod', facing='up') def sweep_root(g, points, radius=1): """Sweep connected oriented timber through an arbitrary polyline.""" for a, b in zip(points, points[1:]): delta = [b[i] - a[i] for i in range(3)] length = max(abs(d) for d in delta) axis = 'xyz'[max(range(3), key=lambda i: abs(delta[i]))] for step in range(length + 1): center = [round(a[i] + delta[i] * step / max(1, length)) for i in range(3)] for dx in range(-radius, radius + 1): for dy in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dy) + abs(dz) <= radius * 2: p = center[0] + dx, max(1, center[1] + dy), center[2] + dz g.put(*p, 'stripped_dark_oak_log' if dx == 0 else 'dark_oak_log', axis=axis) def root_envelope(ctx, mass): """Grow a mound, broad hollowable trunk and radial buttresses before carving rooms.""" g = ctx.g r = mass.rect cx, cz = r.center spread = mass.roots['spread'] crown = mass.roots['crown'] top = mass.levels[-1].floor + mass.levels[-1].height + 2 rx, rz = r.width / 2 + spread, r.depth / 2 + spread for x in range(math.floor(cx - rx), math.ceil(cx + rx) + 1): for z in range(math.floor(cz - rz), math.ceil(cz + rz) + 1): radial = ((x - cx) / rx)**2 + ((z - cz) / rz)**2 if radial >= 1: continue h = 1 + round((top - 1) * (1 - radial)**.5) for y in range(2, h + 1): block = 'moss_block' if y == h else g.rng.choice(('tuff', 'andesite', 'mossy_cobblestone', 'dirt')) g.put(x, y, z, block) # The trunk is a tapered irregular solid, not a box with logs on its corners. for y in range(top - 4, top + crown): radius = max(2, 4 - max(0, y - top) // 2) for x in range(cx - radius, cx + radius + 1): for z in range(cz - radius, cz + radius + 1): if abs(x - cx) + abs(z - cz) <= radius + 1: g.put(x, y, z, 'dark_oak_log', axis='y') count = 7 + (spread % 3) for i in range(count): angle = (i + .15) * math.tau / count dx, dz = math.cos(angle), math.sin(angle) # Skip the entrance sector; its two flanking roots remain wide apart. if dz > .8 and abs(dx) < .35: continue tipx, tipz = round(cx + dx * (rx - 1)), round(cz + dz * (rz - 1)) points = [(cx + round(dx * 2), top + crown - 2, cz + round(dz * 2)), (cx + round(dx * (r.width / 2 - 1)), top - 2, cz + round(dz * (r.depth / 2 - 1))), (tipx, 3, tipz), (tipx + round(dx), 2, tipz + round(dz))] sweep_root(g, points) for sign, lift in ((-1, crown), (1, crown - 1)): sweep_root(g, [(cx + sign, top, cz), (cx + sign * 3, top + lift, cz), (cx + sign * 4, top + lift + 2, cz), (cx + sign * 2, top + lift + 2, cz)], 1) # Carving and the closed irregular room boundary are shared with ordinary envelopes. PY

日志保留的输出末尾

#018 · 2026-09-25T15:15:22.391Z · exit=0 · cat > hearthwright/site.py <<'PY'
cat > hearthwright/site.py <<'PY' # Terrain, outdoor rooms, approaches and adaptable property features. from .blocks import is_air, name, full_support from .materials import DIRECTIONS, OPPOSITE, log from .plan import edge_point def terrain(ctx): """Create bounded land or shallow water before foundations and excavation.""" g, p = ctx.g, ctx.plan site = p.design.site main = p.masses[0].rect for x, z in sorted(p.site.cells()): # Rounded cut corners keep the diorama edge quiet. if min(x - p.site.x0, p.site.x1 - x) + min(z - p.site.z0, p.site.z1 - z) < 2: continue h = 1 if site.terrain == 'slope': h += min(site.rise, max(0, z - main.z0 + 2) // 2) g.put(x, 0, z, g.rng.choice(('stone', 'stone', 'andesite'))) if site.terrain == 'water': edge = min(x - p.site.x0, p.site.x1 - x, z - p.site.z0, p.site.z1 - z) if edge < 2: h = site.water_depth g.box((x, 1, z, x, h - 1, z), 'dirt') g.put(x, h, z, 'grass_block') else: g.put(x, 0, z, g.rng.choice(('mud', 'clay', 'gravel'))) g.box((x, 1, z, x, site.water_depth, z), 'water', level='0') h = 0 else: g.box((x, 1, z, x, h - 1, z), 'dirt') g.put(x, h, z, 'grass_block') ctx.ground[x, z] = h def column(ctx, x, z, top, owner, timber=True): """Take a post or pier to actual ground or waterbed, recording its load path.""" g, mat = ctx.g, ctx.plan.design.materials bottom = ctx.ground.get((x, z), 0) block = log(mat.frame) if timber else 'stone_bricks' g.box((x, bottom, z, x, top, z), block, **({'axis': 'y'} if timber else {})) g.metadata['supports'].append({'owner': owner, 'bottom': (x, bottom, z), 'top': (x, top, z), 'kind': 'post' if timber else 'pier'}) def attachment(ctx, a): """Construct supported outdoor floors, open pergolas or striped market canopies.""" g, mat = ctx.g, ctx.plan.design.materials r, y = a['rect'], a['floor'] block = 'smooth_sandstone' if a['kind'] == 'terrace' else mat.floor + '_planks' for x, z in sorted(r.cells()): g.put(x, y, z, block if (x + z) % 5 else ('terracotta' if a['kind'] == 'terrace' else log(mat.floor))) for x in (r.x0, r.x1): for z in (r.z0, r.z1): column(ctx, x, z, y, a['id'], a['kind'] != 'terrace') side = a['side'] dx, dz = DIRECTIONS[side] cx, cz = a['door'] # Keep a generous center gateway in the rail and an approach to the door. outer = edge_point(r, side) for x, z in sorted(r.cells()): if x not in (r.x0, r.x1) and z not in (r.z0, r.z1): continue on_wall = (side == 'south' and z == r.z0 or side == 'north' and z == r.z1 or side == 'east' and x == r.x0 or side == 'west' and x == r.x1) gate = abs((x - outer[0]) if dz else (z - outer[1])) <= 1 if not on_wall and not gate: g.put(x, y + 1, z, mat.frame + '_fence') g.door(cx, y, cz, side, a['id'], 'warped' if mat.wall == 'white' else 'spruce') ctx.opening_route((cx, y + 1, cz), side, a['id']) centerline = [] for step in range(1, max(r.width, r.depth) + 1): x, z = cx + dx * step, cz + dz * step if not r.contains(x, z): break g.box((x, y + 1, z, x, y + 2, z), 'air') centerline.append((x, y + 1, z)) g.route(centerline, a['id']) a['target'] = centerline[len(centerline) // 2] if a['cover'] != 'open': beam = y + 5 for x in (r.x0, r.x1): for z in (r.z0, r.z1): g.box((x, y + 1, z, x, beam, z), log(mat.frame), axis='y') for z in (r.z0, r.z1): g.box((r.x0, beam, z, r.x1, beam, z), log(mat.frame), axis='x') for x in (r.x0, r.x1): g.box((x, beam, r.z0, x, beam, r.z1), log(mat.frame), axis='z') if a['cover'] == 'pergola': for z in range(r.z0, r.z1 + 1, 2): g.box((r.x0, beam + 1, z, r.x1, beam + 1, z), mat.floor + '_slab', type='bottom') else: for x, z in sorted(r.cells()): g.put(x, beam, z, 'red_wool' if (x - r.x0) % 4 < 2 else 'white_wool') g.lantern(r.x0 + 1, beam - 1, r.z0, a['id'], True) else: g.put(r.x0, y + 1, r.z0, mat.frame + '_fence') g.lantern(r.x0, y + 2, r.z0, a['id']) # Domestic furniture at the edge, separated from the reserved entry axis. fx, fz = r.x0 + 1, r.z0 + 1 if (fx, y + 1, fz) not in g.reservations: g.put(fx, y + 1, fz, 'barrel' if a['kind'] == 'market' else mat.floor + '_stairs', **({'facing': 'south'})) if a['kind'] == 'market': g.put(fx, y + 2, fz, 'melon') g.inventory((fx, y + 1, fz), [('emerald', 8), ('bread', 12)]) g.metadata['features'].append({'owner': a['id'], 'kind': a['kind'], 'target': a['target']}) def approach(ctx): """Connect a dry public start to the entrance with supported, head-clear steps.""" g, p, mat = ctx.g, ctx.plan, ctx.plan.design.materials e = p.entrance x, z = e['point'] floor = e['floor'] side = e['side'] dx, dz = DIRECTIONS[side] start_distance = 1 # A matching terrace becomes the upper landing; stairs begin at its outer edge. for a in p.attachments: if a['to'] == e['volume'] and a['level'] == e['level'] and a['side'] == side and a['rect'].contains(x + dx, z + dz): r = a['rect'] start_distance = (r.depth if dz else r.width) + 1 landing_floor = p.design.site.water_depth + 1 if p.design.site.terrain == 'water' else 1 end_distance = start_distance + max(0, floor - landing_floor) + 4 points = [] for d in range(1, end_distance + 1): px, pz = x + dx * d, z + dz * d tread = max(landing_floor, floor - max(0, d - start_distance + 1)) is_step = d >= start_distance and tread > landing_floor - 1 and d < start_distance + (floor - landing_floor) for w in (-1, 0, 1): xx, zz = px + (w if dz else 0), pz + (w if dx else 0) g.box((xx, 1, zz, xx, tread - 1, zz), 'stone_bricks' if p.design.site.terrain != 'water' else mat.floor + '_planks') g.put(xx, tread, zz, mat.floor + '_planks' if p.design.site.terrain == 'water' else 'stone_bricks') if is_step: g.stair(xx, tread, zz, mat.floor if p.design.site.terrain == 'water' else 'stone_brick', OPPOSITE[side]) g.metadata['stairs'].append({'owner': 'approach', 'point': (xx, tread, zz), 'facing': OPPOSITE[side]}) g.box((xx, tread + 1, zz, xx, max(tread + 3, ctx.ground.get((xx, zz), 1) + 2), zz), 'air') points.append((px, tread + 1, pz)) # The final upper tread occupies the entrance landing at the same elevation. if floor > landing_floor: d = start_distance - 1 if d >= 1: px, pz = x + dx * d, z + dz * d for w in (-1, 0, 1): xx, zz = px + (w if dz else 0), pz + (w if dx else 0) g.stair(xx, floor, zz, mat.floor if p.design.site.terrain == 'water' else 'stone_brick', OPPOSITE[side]) g.metadata['stairs'].append({'owner': 'approach', 'point': (xx, floor, zz), 'facing': OPPOSITE[side]}) else: # Entrance threshold is a final supported stair, with the door standing on it. # Leave the actual door on a full block: insert the top tread immediately outside. for w in (-1, 0, 1): xx, zz = x + dx + (w if dz else 0), z + dz + (w if dx else 0) g.stair(xx, floor, zz, mat.floor if p.design.site.terrain == 'water' else 'stone_brick', OPPOSITE[side]) points[0] = (x + dx, floor + 1, z + dz) # Shift remaining risers out by one cell to preserve successive one-block rises. for i in range(1, len(points)): px, _, pz = points[i] yy = max(landing_floor, floor - i) for w in (-1, 0, 1): xx, zz = px + (w if dz else 0), pz + (w if dx else 0) g.box((xx, 1, zz, xx, yy - 1, zz), 'stone_bricks') g.put(xx, yy, zz, 'stone_bricks') if yy > landing_floor: g.stair(xx, yy, zz, 'stone_brick', OPPOSITE[side]) g.box((xx, yy + 1, zz, xx, yy + 3, zz), 'air') points[i] = (px, yy + 1, pz) g.route(points, 'approach') ctx.start = points[-1] g.metadata['start'] = ctx.start g.metadata['entrance'] = (x, floor + 1, z) for d in (2, end_distance): px, pz = x + dx * d + (3 if dz else 0), z + dz * d + (3 if dx else 0) y = ctx.ground.get((px, pz), 1) if p.design.site.terrain == 'water': column(ctx, px, pz, landing_floor, 'approach-light') y = landing_floor if is_air(g.get(px, y + 1, pz)): g.put(px, y + 1, pz, 'stone_brick_wall') g.lantern(px, y + 2, pz, 'approach') def feature(ctx, f): """Realize a reserved feature plot with contained water, supports and access.""" g, p, mat = ctx.g, ctx.plan, ctx.plan.design.materials r, kind = f['rect'], f['kind'] cx, cz = r.center # All working plots get an engineered level platform on their terrain. y = max(ctx.ground.get(q, 1) for q in r.cells()) if p.design.site.terrain == 'water': y = p.design.site.water_depth + 1 if kind in ('landing',): block = mat.floor + '_planks' else: block = 'grass_block' for x, z in sorted(r.cells()): g.box((x, 1, z, x, y - 1, z), 'dirt' if kind != 'landing' else mat.floor + '_planks') g.put(x, y, z, block) if kind == 'pool': for x, z in sorted(r.cells()): edge = x in (r.x0, r.x1) or z in (r.z0, r.z1) g.put(x, y, z, 'smooth_quartz' if edge else 'water', **({} if edge else {'level': '0'})) if not edge: g.put(x, y - 1, z, 'prismarine_bricks') # Two loungers occupy the dry coping edge. for x in (r.x0 + 1, r.x1 - 1): g.stair(x, y + 1, r.z1, 'birch', 'north') elif kind == 'field': for x, z in sorted(r.inset(1).cells()): if x == cx: g.put(x, y, z, 'water', level='0') else: g.put(x, y, z, 'farmland', moisture='7') g.put(x, y + 1, z, 'wheat', age=str(g.rng.choice((5, 7, 7, 7)))) for x, z in sorted(r.cells() - r.inset(1).cells()): if (x, z) != (cx, r.z1): g.put(x, y + 1, z, 'stone_brick_slab', type='bottom') elif kind == 'well': g.box((cx - 1, y, cz - 1, cx + 1, y + 1, cz + 1), 'stone_bricks') g.put(cx, y + 1, cz, 'water', level='0') for x in (cx - 2, cx + 2): for z in (cz - 2, cz + 2): g.box((x, y + 1, z, x, y + 5, z), log(mat.frame), axis='y') for x in range(cx - 3, cx + 4): for z in range(cz - 3, cz + 4): g.put(x, y + 6, z, mat.floor + '_slab', type='bottom') g.box((cx - 2, y + 4, cz, cx + 2, y + 4, cz), log(mat.frame), axis='x') g.put(cx, y + 3, cz, 'chain', axis='y') elif kind == 'windmill': g.box((cx - 1, y + 1, cz - 1, cx + 1, y + 5, cz + 1), 'stone_bricks') g.box((cx, y + 6, cz, cx, y + 11, cz), log(mat.frame), axis='y') g.box((cx - 1, y + 11, cz - 1, cx + 1, y + 11, cz + 1), mat.floor + '_slab', type='bottom') sy, sz = y + 8, cz + 2 g.put(cx, sy, cz + 1, log(mat.frame), axis='z') for step in range(-4, 5): g.put(cx + step, sy, sz, log(mat.frame), axis='x') g.put(cx, sy + step, sz, log(mat.frame), axis='y') for d in (2, 3, 4): for width in (1,): for xx, yy in ((cx + width, sy + d), (cx + d, sy - width), (cx - width, sy - d), (cx - d, sy + width)): g.put(xx, yy, sz, 'oak_trapdoor', facing='south', open='true') g.put(cx + 1, y + 1, cz + 2, 'grindstone', face='floor', facing='south') elif kind == 'landing': for x in (r.x0, r.x1): for z in (r.z0, r.z1): column(ctx, x, z, y + 1, f['id']) g.put(r.x1, y + 2, r.z1, 'lantern', hanging='false') g.put(r.x0 + 1, y + 1, r.z0 + 1, 'barrel') g.inventory((r.x0 + 1, y + 1, r.z0 + 1), [('oak_boat', 1), ('fishing_rod', 1), ('lead', 2)]) # A low mooring arm has visible water beside the bollard. for x in range(r.x1 + 1, r.x1 + 3): g.put(x, y, r.z1, mat.floor + '_planks') column(ctx, r.x1 + 2, r.z1, y + 1, f['id']) else: for x, z in sorted(r.inset(1).cells()): if (x + z) % 2 == 0: g.put(x, y + 1, z, g.rng.choice(('allium', 'cornflower', 'oxeye_daisy'))) f['target'] = (cx, y + 1, r.z1 + 1) f['floor'] = y g.metadata['features'].append({'owner': f['id'], 'kind': kind, 'target': f['target']}) # Lit working edge outside the entrance into the feature. g.put(r.x0, y + 1, r.z1, 'stone_brick_wall') g.lantern(r.x0, y + 2, r.z1, f['id']) def connect_features(ctx): """Route exterior walks around occupied footprints using a bounded grid search.""" from collections import deque g, p = ctx.g, ctx.plan start = ctx.start[0], ctx.start[2] forbidden = set() for mass in p.masses: forbidden |= mass.rect.inset(-2 if mass.envelope == 'framed' else -mass.roots['spread']).cells() for a in p.attachments: forbidden |= a['rect'].cells() for f in p.features: forbidden |= f['rect'].cells() targets = [(f['target'], f['id']) for f in p.features] # Secondary upper terraces connect through their own doors; they need no ground stair. for target, owner in targets: target2 = target[0], target[2] forbidden.discard(target2) parents = {start: None} queue = deque([start]) while queue and target2 not in parents: x, z = queue.popleft() for dx, dz in DIRECTIONS.values(): q = x + dx, z + dz if q not in parents and q not in forbidden and p.site.contains(*q): parents[q] = (x, z) queue.append(q) if target2 not in parents: raise ValueError(f'{owner}: no clear exterior approach; change its side') path = [] q = target2 while q is not None: path.append(q) q = parents[q] path.reverse() # Paths use the entrance landing elevation. Terraced rises use a dedicated last run. start_y = ctx.start[1] - 1 target_y = target[1] - 1 difference = target_y - start_y if abs(difference) > len(path) - 2: raise ValueError(f'{owner}: path too short for terrain transition') points = [] for i, (x, z) in enumerate(path): h = start_y remaining = len(path) - 1 - i if remaining < abs(difference): h += (1 if difference > 0 else -1) * (abs(difference) - remaining) g.box((x, 0, z, x, h - 1, z), 'stone_bricks' if p.design.site.terrain != 'water' else 'spruce_planks') g.put(x, h, z, 'coarse_dirt' if p.design.site.terrain != 'water' else 'spruce_planks') g.box((x, h + 1, z, x, max(h + 3, ctx.ground.get((x, z), 1) + 2), z), 'air') if difference > 0 and remaining < difference: px, pz = path[i - 1] facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - px, z - pz)) g.stair(x, h, z, 'stone_brick', facing) elif difference < 0 and remaining <= -difference and i + 1 < len(path): nx, nz = path[i + 1] facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - nx, z - nz)) g.stair(x, h, z, 'stone_brick', facing) points.append((x, h + 1, z)) g.route(points, owner) def planting(ctx): """Add restrained gardens and trees only in genuinely free site margins.""" g, p = ctx.g, ctx.plan policy = p.design.site.planting forbidden = set() for m in p.masses: forbidden |= m.rect.inset(-3).cells() for a in p.attachments + p.features: forbidden |= a['rect'].inset(-2).cells() for route in g.metadata['routes']: for x, _, z in route['points']: forbidden |= {(x + dx, z + dz) for dx in range(-2, 3) for dz in range(-2, 3)} candidates = [(x, z) for x, z in sorted(p.site.inset(3).cells()) if (x, z) not in forbidden and name(g.get(x, ctx.ground.get((x, z), 0), z)) == 'grass_block'] tree_count = 0 if policy == 'sparse' else 2 g.rng.shuffle(candidates) trunks = [] for x, z in candidates: if len(trunks) >= tree_count: break if any(abs(x - tx) + abs(z - tz) < 10 for tx, tz in trunks): continue if any((x + dx, z + dz) in forbidden for dx in range(-2, 3) for dz in range(-2, 3)): continue tree(ctx, x, z, policy) trunks.append((x, z)) for x, z in sorted(p.site.inset(1).cells()): if (x, z) in forbidden: continue y = ctx.ground.get((x, z), 0) + 1 if not is_air(g.get(x, y, z)): continue if name(g.get(x, y - 1, z)) == 'grass_block' and g.rng.random() < .12: g.put(x, y, z, g.rng.choice(('short_grass', 'short_grass', 'fern', 'cornflower', 'oxeye_daisy'))) elif p.design.site.terrain == 'water' and name(g.get(x, p.design.site.water_depth, z)) == 'water' and g.rng.random() < .035: g.put(x, p.design.site.water_depth + 1, z, 'lily_pad') def tree(ctx, x, z, policy): """Grow a small connected tree using the site's planting vocabulary.""" g = ctx.g base = ctx.ground[x, z] height = g.rng.randint(7, 10) if policy == 'mediterranean' else g.rng.randint(5, 8) wood = 'jungle' if policy == 'mediterranean' else 'spruce' if policy == 'conifer' else 'oak' g.box((x, base + 1, z, x, base + height, z), wood + '_log', axis='y') leaves = set() if policy == 'mediterranean': for dx, dz in DIRECTIONS.values(): for d in range(4): leaves.add((x + dx * d, base + height + (1 if d < 3 else 0), z + dz * d)) if d == 3: leaves.add((x + dx * d, base + height - 1, z + dz * d)) else: for dy in range(-3, 2): radius = max(0, 2 - abs(dy) // 2) if policy != 'conifer' else (2 if dy % 2 == 0 else 1) for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dz) <= radius + 1: leaves.add((x + dx, base + height + dy, z + dz)) for point in sorted(leaves): if is_air(g.get(*point)): g.put(*point, wood + '_leaves', persistent='true', distance='1') PY

日志保留的输出末尾

#019 · 2026-09-25T15:18:25.013Z · exit=0 · cat > hearthwright/build.py <<'PY'
cat > hearthwright/build.py <<'PY' # Compile a resolved architectural plan into furnished, reserved block geometry. from __future__ import annotations from collections import deque from dataclasses import asdict import hashlib import json from pathlib import Path from .api import Design, DesignError from .blocks import Grid, is_air, passable, name from .materials import WALLS, DIRECTIONS, OPPOSITE, log from .plan import resolve, edge_point from . import site, roofs from .navigation import flood, path_to, walkable class Compiler: """Share construction state and spatial reservations across reusable components.""" def __init__(self, plan): self.plan = plan self.g = Grid(plan.seed) self.ground = {} self.interior = set() self.inner = {} self.rooms = [] self.start = None self.roof_maps = {} for mass in plan.masses: for s in mass.levels: self.inner[s.id] = {(x, z) for x, z in s.cells if all((x + dx, z + dz) in s.cells for dx, dz in DIRECTIONS.values())} def opening_route(self, p, side, owner): x, y, z = p dx, dz = DIRECTIONS[side] points = [(x + dx * d, y, z + dz * d) for d in (-1, 0, 1)] self.g.route(points, owner) def construct(self): """Run geometry passes in dependency order, reserving circulation before furniture.""" site.terrain(self) for m in self.plan.masses: if m.envelope == 'roots': roofs.root_envelope(self, m) for m in self.plan.masses: self.envelope(m) roofs.build_roofs(self) for m in self.plan.masses: self.partitions(m) self.circulation(m) self.openings() for a in self.plan.attachments: site.attachment(self, a) site.approach(self) for f in self.plan.features: site.feature(self, f) site.connect_features(self) self.reserve_rooms() from .furnish import furnish furnish(self) site.planting(self) self.connect_fences() self.g.metadata['interior'] = sorted(self.interior) self.g.metadata['resolved'] = self.plan.summary() self.g.metadata['structural_signature'] = self.plan.signature() self.g.metadata['geometry_signature'] = self.geometry_signature() self.g.metadata['limits'] = asdict(self.plan.design.limits) self.g.metadata['seed'] = self.plan.seed self.g.metadata['name'] = self.plan.design.name self.check_limits() return self.g def envelope(self, mass): """Excavate, frame and seal arbitrary stacked footprints on real foundations.""" g, mat = self.g, self.plan.design.materials base = mass.levels[0] if mass.supports == 'foundation': for x, z in sorted(base.cells): g.box((x, 1, z, x, base.floor - 1, z), 'stone_bricks') else: r = base.rect posts = {(r.x0, r.z0), (r.x1, r.z0), (r.x0, r.z1), (r.x1, r.z1)} for x in range(r.x0, r.x1 + 1, 5): posts |= {(x, r.z0), (x, r.z1)} for z in range(r.z0, r.z1 + 1, 5): posts |= {(r.x0, z), (r.x1, z)} for x, z in sorted(posts): site.column(self, x, z, base.floor - 1, mass.name) for z in (r.z0, r.z1): g.box((r.x0, base.floor - 1, z, r.x1, base.floor - 1, z), log(mat.frame), axis='x') for i, s in enumerate(mass.levels): inner = self.inner[s.id] for x, z in sorted(s.cells): g.put(x, s.floor, z, mat.floor + '_planks' if (x, z) in inner else 'stone_bricks' if i == 0 else log(mat.frame)) if (x, z) in inner: g.box((x, s.floor + 1, z, x, s.floor + s.height - 1, z), 'air') self.interior.update((x, y, z) for y in range(s.floor + 1, s.floor + s.height)) else: for y in range(s.floor + 1, s.floor + s.height): # Root envelopes use a continuous irregular stone lining. frame = mass.envelope == 'framed' and ((x in (s.rect.x0, s.rect.x1) and (z - s.rect.z0) % 5 == 0) or (z in (s.rect.z0, s.rect.z1) and (x - s.rect.x0) % 5 == 0)) g.put(x, y, z, log(mat.frame) if frame else g.rng.choice(WALLS[s.wall]), **({'axis': 'y'} if frame else {})) if not s.attic: g.put(x, s.floor + s.height, z, mat.floor + '_planks') # Full perimeter sill and top beam make the frame readable from every side. if mass.envelope == 'framed': for x, z in sorted(s.cells - inner): g.put(x, s.floor + s.height - 1, z, log(mat.frame), axis='x' if z in (s.rect.z0, s.rect.z1) else 'z') if i and s.cells - mass.levels[i - 1].cells: below = mass.levels[i - 1] # An upper overhang is carried by full posts and high knee braces. r = s.rect points = {(r.x0, r.z0), (r.x1, r.z0), (r.x0, r.z1), (r.x1, r.z1), (r.center[0], r.z0), (r.center[0], r.z1)} for x, z in sorted(points): if (x, z) in below.cells: continue site.column(self, x, z, s.floor - 1, s.id) for dx, dz in DIRECTIONS.values(): if (x + dx, z + dz) in s.cells and (x + dx, z + dz) not in below.cells: g.stair(x + dx, s.floor - 1, z + dz, mat.floor, next(k for k, delta in DIRECTIONS.items() if delta == (-dx, -dz)), 'top') for z in (r.z0, r.z1): g.box((r.x0, s.floor - 1, z, r.x1, s.floor - 1, z), log(mat.frame), axis='x') for x in (r.x0, r.x1): g.box((x, s.floor - 1, r.z0, x, s.floor - 1, r.z1), log(mat.frame), axis='z') def partitions(self, mass): """Subdivide selected levels into functional rooms with real door connections.""" g = self.g for s in mass.levels: inner = self.inner[s.id] groups = [inner] programs = [s.functions] if s.layout != 'open': axis = 0 if s.layout == 'cross' else 1 middle = s.rect.center[axis] wall = {(x, z) for x, z in inner if (x, z)[axis] == middle} for x, z in sorted(wall): ceiling = self.roof_maps.get(mass.name, {}).get((x, z), {}).get('height', s.floor + s.height) if s.attic else s.floor + s.height g.box((x, s.floor + 1, z, x, ceiling - 1, z), 'smooth_sandstone' if s.wall in ('plaster', 'white') else 'oak_planks') cx, cz = s.rect.center g.door(cx, s.floor, cz, 'east' if axis == 0 else 'south', s.id) self.opening_route((cx, s.floor + 1, cz), 'east' if axis == 0 else 'south', s.id) groups = [{p for p in inner if p[axis] < middle}, {p for p in inner if p[axis] > middle}] split = (len(s.functions) + 1) // 2 programs = [s.functions[:split], s.functions[split:]] for index, (cells, functions) in enumerate(zip(groups, programs)): self.rooms.append({'id': f'{s.id}/room{index}', 'level': s.id, 'floor': s.floor, 'height': s.height, 'cells': cells, 'functions': functions}) def circulation(self, mass): """Build complete stairwells or backed ladders between consecutive floor masks.""" g = self.g for low, high in zip(mass.levels, mass.levels[1:]): common = self.inner[low.id] & self.inner[high.id] owner = f'{low.id}->{high.id}' delta = high.floor - low.floor if mass.circulation == 'ladder': candidates = [(x, z) for x, z in sorted(common) if all((x, yy, z) in self.interior for yy in (low.floor + 1, high.floor + 1)) and (x - 1, z) not in common] if not candidates: raise DesignError(f'{owner}: no shared ladder shaft') x, z = min(candidates, key=lambda p: abs(p[1] - low.rect.center[1])) g.box((x - 1, low.floor + 1, z, x - 1, high.floor + 2, z), 'spruce_planks') for y in range(low.floor + 1, high.floor + 2): g.put(x, y, z, 'ladder', facing='east') g.put(x, high.floor + 2, z, 'air') self.interior.update((x, y, z) for y in range(low.floor + 1, high.floor + 3)) g.route([(x, y, z) for y in range(low.floor + 1, high.floor + 2)], owner) continue x = max(low.rect.x0, high.rect.x0) + 1 z0 = max(low.rect.z0, high.rect.z0) + 2 points = [] for i in range(delta): z = z0 + i y = low.floor + i + 1 for xx in (x, x + 1): if (xx, z) not in common: raise DesignError(f'{owner}: staircase exceeds the shared floor, use ladder or enlarge depth') g.box((xx, low.floor + 1, z, xx, y - 1, z), 'spruce_planks') g.stair(xx, y, z, 'spruce', 'south') g.box((xx, y + 1, z, xx, y + 3, z), 'air') self.interior.update((xx, yy, z) for yy in range(y + 1, y + 4)) points.append((xx, y + 1, z)) g.metadata['stairs'].append({'owner': owner, 'point': (xx, y, z), 'facing': 'south'}) for yy, z in ((low.floor + 1, z0 - 1), (high.floor + 1, z0 + delta)): for xx in (x, x + 1): g.box((xx, yy, z, xx, yy + 2, z), 'air') points.append((xx, yy, z)) # Guard the upper opening without blocking the top or bottom landing. for z in range(z0 + max(1, delta - 3), z0 + delta): if is_air(g.get(x + 2, high.floor + 1, z)): g.put(x + 2, high.floor + 1, z, 'spruce_fence') g.route(points, owner) def openings(self): """Align glazed bays with all free facades, then cut registered doors.""" g, p, mat = self.g, self.plan, self.plan.design.materials doors = [] for m in p.masses: if m.join: j = m.join doors.append((j['point'], j['floor'], j['side'], m.name + '/join')) e = p.entrance doors.append((e['point'], e['floor'], e['side'], e['volume'] + '/entrance')) for a in p.attachments: doors.append((a['door'], a['floor'], a['side'], a['id'])) for m in p.masses: for s in m.levels: r = s.rect for side in DIRECTIONS: dx, dz = DIRECTIONS[side] span = r.width if dz else r.depth offsets = [span // 2] if span <= 11 else [span // 3, 2 * span // 3] for offset in offsets: x, z = ((r.x0 + offset, r.z1 if side == 'south' else r.z0) if dz else (r.x1 if side == 'east' else r.x0, r.z0 + offset)) if (x, z) not in s.cells or (x - dx, z - dz) not in self.inner[s.id]: continue if any(abs(x - q[0]) + abs(z - q[1]) < 3 and floor == s.floor for q, floor, _, _ in doors): continue if any(other.name != m.name and any(l.rect.contains(x + dx, z + dz) and l.floor <= s.floor + 2 <= l.floor + l.height for l in other.levels) for other in p.masses): continue yy = s.floor + 2 points = [(x, yy, z), (x, yy + 1, z)] if s.height >= 5 else [(x, yy, z)] for pt in points: g.put(*pt, 'light_blue_stained_glass' if mat.wall == 'white' else 'glass') g.metadata['openings'].append({'kind': 'window', 'owner': s.id, 'cells': points, 'side': side}) # Daylight wells through thick hills preserve the original sealed window. distance = m.roots['spread'] + 2 if m.envelope == 'roots' else 2 if p.design.site.terrain == 'slope' else 1 for d in range(1, distance + 1): px, pz = x + dx * d, z + dz * d g.box((px, yy, pz, px, yy + len(points) - 1, pz), 'air') g.reserve([(x - dx, y, z - dz) for _, y, _ in points], s.id, 'window') if mat.shutters and m.envelope != 'roots' and s.height >= 5: tx, tz = (1, 0) if dz else (0, 1) for sign in (-1, 1): for y in range(yy, yy + len(points)): px, pz = x + dx + tx * sign, z + dz + tz * sign if is_air(g.get(px, y, pz)): g.put(px, y, pz, ('warped' if mat.wall == 'white' else mat.frame) + '_trapdoor', facing=side, open='true') # Small flower box attaches to the wall under the sill. if m.envelope != 'roots' and not any(a['floor'] == s.floor and a['rect'].contains(x + dx, z + dz) for a in p.attachments): g.put(x + dx, s.floor, z + dz, 'dirt') g.put(x + dx, s.floor + 1, z + dz, 'cornflower' if mat.accent in ('blue', 'cyan') else 'poppy') g.put(x + 2 * dx, s.floor, z + 2 * dz, mat.frame + '_trapdoor', facing=side, open='true') g.stair(x + dx, s.floor - 1, z + dz, mat.floor, OPPOSITE[side], 'top') for (x, z), floor, side, owner in doors: g.door(x, floor, z, side, owner, 'warped' if mat.wall == 'white' else 'spruce') dx, dz = DIRECTIONS[side] for distance in (-1, 1): xx, zz = x + dx * distance, z + dz * distance g.box((xx, floor + 1, zz, xx, floor + 2, zz), 'air') if is_air(g.get(xx, floor, zz)) or name(g.get(xx, floor, zz)) == 'water': g.put(xx, floor, zz, mat.floor + '_planks') self.opening_route((x, floor + 1, z), side, owner) # Recessed entrance surround projects one block and stays three blocks high. x, z = e['point'] y = e['floor'] dx, dz = DIRECTIONS[e['side']] for w in (-2, 2): xx, zz = x + dx + (w if dz else 0), z + dz + (w if dx else 0) g.box((xx, y, zz, xx, y + 3, zz), log(mat.frame) if mat.wall != 'white' else 'smooth_sandstone') for w in range(-2, 3): xx, zz = x + dx + (w if dz else 0), z + dz + (w if dx else 0) g.put(xx, y + 3, zz, mat.floor + '_slab', type='top') # Lamp beside the doorway is supported by a real sill. xx, zz = x + dx + (2 if dz else 0), z + dz + (2 if dx else 0) g.lantern(xx, y + 4, zz, 'entrance') def reserve_rooms(self): """Derive protected paths through the actual empty structure before decoration.""" g = self.g parents = flood(g, self.start) if tuple(g.metadata['entrance']) not in parents: raise DesignError('approach does not reach entrance in actual geometry') for room in self.rooms: cx = sum(x for x, z in room['cells']) / len(room['cells']) cz = sum(z for x, z in room['cells']) / len(room['cells']) candidates = [(x, room['floor'] + 1, z) for x, z in room['cells'] if (x, room['floor'] + 1, z) in parents] if not candidates: raise DesignError(f'{room["id"]}: no reachable floor; review its circulation or join') anchor = min(candidates, key=lambda p: (abs(p[0] - cx) + abs(p[2] - cz), p)) room['anchor'] = anchor g.route(path_to(parents, anchor), room['id']) g.metadata['rooms'].append({**room, 'cells': sorted(room['cells'])}) for f in g.metadata['features']: if tuple(f['target']) not in parents: raise DesignError(f'{f["owner"]}: outdoor target is not reachable') g.route(path_to(parents, f['target']), f['owner']) def connect_fences(self): """Resolve explicit fence connections so exports render and paste consistently.""" g = self.g for (x, y, z), state in list(g.blocks.items()): if name(state).endswith('_fence'): properties = {} for face, (dx, dz) in DIRECTIONS.items(): n = name(g.get(x + dx, y, z + dz)) properties[face] = str(n.endswith(('_fence', '_log', '_planks', '_wood'))).lower() g.put(x, y, z, name(state), **properties) def geometry_signature(self): """Digest actual structural cells, excluding texture, plants and cosmetic materials.""" # Geometry classifications retain roof/stair topology, floor masks and partitions. selected = [] for mass in self.plan.masses: r = mass.rect.inset(-2) bottom = mass.levels[0].floor for (x, y, z), state in sorted(self.g.blocks.items()): if not r.contains(x, z) or y < bottom: continue n = name(state) role = 'door' if n.endswith('_door') else 'glass' if 'glass' in n else 'stair' if n.endswith('_stairs') else 'ladder' if n == 'ladder' else 'solid' if n.endswith(('_planks', '_log', '_stem')) or n in ('stone_bricks', 'andesite', 'stone', 'cobblestone', 'calcite', 'smooth_quartz', 'smooth_sandstone', 'deepslate_tiles', 'mangrove_planks', 'bricks', 'light_blue_concrete', 'light_blue_terracotta', 'tuff', 'mossy_cobblestone', 'white_terracotta') else None if role: selected.append((x, y, z, role)) return hashlib.sha256(json.dumps(selected, separators=(',', ':')).encode()).hexdigest() def check_limits(self): b = self.g.bounds size = [b[i + 3] - b[i] + 3 for i in range(3)] limits = self.plan.design.limits if max(size[0], size[2]) > limits.horizontal or size[1] > limits.vertical or size[0] * size[1] * size[2] > limits.volume or len(self.g.blocks) > limits.blocks: raise DesignError(f'final geometry size {size} / {len(self.g.blocks)} blocks exceeds Limits; increase resource limits or reduce the composition') class Building: """Expose a generated artifact, design metadata, validation and deterministic export.""" def __init__(self, grid, plan=None): self.grid = grid self.plan = plan @property def metadata(self): return self.grid.metadata def validate(self): from .validate import validate return validate(self) def export(self, path: Path): self.validate().require_valid() return self.grid.export(Path(path), self.metadata['name']) def verify(self, path: Path): return self.grid.verify(Path(path)) @classmethod def load(cls, path: Path, metadata=None): return cls(Grid.load(Path(path), metadata)) def generate(design: Design, seed: int, *, validate=True) -> Building: """Resolve, construct and optionally validate one user-composed property.""" plan = resolve(design, seed) artifact = Building(Compiler(plan).construct(), plan) if validate: artifact.validate().require_valid() return artifact PY

日志保留的输出末尾

#020 · 2026-09-25T15:21:57.870Z · exit=0 · cat > hearthwright/furnish.py <<'PY'
cat > hearthwright/furnish.py <<'PY' # Fit reusable domestic furniture groups around reserved circulation and daylight. from collections import deque from .api import DesignError from .blocks import is_air, full_support from .materials import DIRECTIONS from .navigation import flood, path_to def modules(function, mat): """Translate room functions into useful compact furniture groups.""" color = mat.accent sofa = [(0, 0, 0, mat.floor + '_stairs', {'facing': 'north'}), (1, 0, 0, mat.floor + '_stairs', {'facing': 'north'})] programs = { 'living': [('settle', sofa), ('table', [(0, 0, 0, mat.floor + '_planks', {}), (0, 1, 0, 'potted_fern', {})])], 'kitchen': [('cooking', [(0, 0, 0, 'smoker', {'facing': 'south'}), (1, 0, 0, 'crafting_table', {}), (2, 0, 0, 'water_cauldron', {'level': '3'})])], 'sleeping': [('bed', [(0, 0, 0, color + '_bed', {'part': 'foot', 'facing': 'north'}), (0, 0, -1, color + '_bed', {'part': 'head', 'facing': 'north'})]), ('bedside', [(0, 0, 0, 'barrel', {'facing': 'up'})])], 'storage': [('stores', [(0, 0, 0, 'barrel', {'facing': 'south'}), (1, 0, 0, 'barrel', {'facing': 'south'})])], 'workshop': [('workbench', [(0, 0, 0, 'crafting_table', {}), (1, 0, 0, 'smithing_table', {})]), ('toolbox', [(0, 0, 0, 'barrel', {'facing': 'up'})])], 'study': [('bookcase', [(0, 0, 0, 'bookshelf', {}), (0, 1, 0, 'bookshelf', {})]), ('desk', [(0, 0, 0, 'cartography_table', {})])], 'enchanting': [('enchantment', [(0, 0, 0, 'enchanting_table', {})]), ('books', [(0, 0, 0, 'bookshelf', {}), (1, 0, 0, 'bookshelf', {})])], 'brewing': [('alchemy', [(0, 0, 0, 'smooth_stone', {}), (0, 1, 0, 'brewing_stand', {}), (1, 0, 0, 'water_cauldron', {'level': '3'})])], 'trading': [('counter', [(0, 0, 0, 'barrel', {'facing': 'south'}), (1, 0, 0, 'cartography_table', {})])], 'fishing': [('tackle', [(0, 0, 0, 'barrel', {'facing': 'up'}), (1, 0, 0, 'smoker', {'facing': 'south'})])], } return programs[function] def rotate_piece(piece, rotation): """Rotate a furnishing in plan, including bed halves and chair facings.""" dx, dy, dz, block, properties = piece props = dict(properties) for _ in range(rotation): dx, dz = -dz, dx if 'facing' in props and props['facing'] in DIRECTIONS: props['facing'] = {'north': 'east', 'east': 'south', 'south': 'west', 'west': 'north'}[props['facing']] return dx, dy, dz, block, props def place_group(ctx, room, kind, pieces, function): """Find a supported layout and reserve a reachable interaction point before placement.""" g = ctx.g floor = room['floor'] cells = room['cells'] allowed = {(x, floor + 1, z) for x, z in cells} candidates = [(x, z, rot) for x, z in sorted(cells) for rot in range(4)] g.rng.shuffle(candidates) # Favor corners and walls, while retaining randomized tie-breaks. candidates.sort(key=lambda p: sum((p[0] + dx, p[1] + dz) in cells for dx, dz in DIRECTIONS.values())) for x, z, rotation in candidates: parts = [rotate_piece(piece, rotation) for piece in pieces] footprint = {(x + dx, z + dz) for dx, _, dz, _, _ in parts} if not footprint <= cells: continue occupied = [(x + dx, floor + 1 + dy, z + dz) for dx, dy, dz, _, _ in parts] clearance = [(px, floor + dy, pz) for px, pz in footprint for dy in (1, 2)] if not g.free(occupied) or not g.free(clearance) or any(not full_support(g.get(px, floor, pz)) for px, pz in footprint): continue parents = flood(g, room['anchor'], allowed - {(px, floor + 1, pz) for px, pz in footprint}, maximum=2000) contacts = [(px + dx, floor + 1, pz + dz) for px, pz in sorted(footprint) for dx, dz in DIRECTIONS.values() if (px + dx, floor + 1, pz + dz) in parents] if not contacts: continue access = min(contacts, key=lambda p: (abs(p[0] - room['anchor'][0]) + abs(p[2] - room['anchor'][2]), p)) g.route(path_to(parents, access), room['id'] + '/' + kind) for dx, dy, dz, block, props in parts: point = x + dx, floor + 1 + dy, z + dz g.put(*point, block, **props) if block in ('barrel', 'smoker'): supplies = [('fishing_rod', 1), ('cod', 6), ('string', 12)] if function == 'fishing' else [('emerald', 8), ('paper', 12)] if function == 'trading' else [('bread', 6), ('oak_log', 12)] g.inventory(point, supplies) g.metadata['furniture'].append({'owner': room['id'], 'function': function, 'kind': kind, 'cells': occupied, 'access': access, 'states': [g.get(*p) for p in occupied]}) return raise DesignError(f'{room["id"]}: cannot fit {function}/{kind} with clear access; enlarge the floor or reduce this room program') def furnish(ctx): """Equip every program, illuminate every room and keep the protected routes clear.""" g, mat = ctx.g, ctx.plan.design.materials for room in ctx.rooms: # Fit long and multi-block pieces first, before optional decor. groups = [(kind, pieces, function) for function in room['functions'] for kind, pieces in modules(function, mat)] groups.sort(key=lambda item: -len({(p[0], p[2]) for p in item[1]})) for kind, pieces, function in groups: place_group(ctx, room, kind, pieces, function) # The lamp module carries its own real support, including in irregular attics. place_group(ctx, room, 'lamp', [(0, 0, 0, mat.floor + '_planks', {}), (0, 1, 0, 'lantern', {'hanging': 'false'})], 'lighting') lamp = g.metadata['furniture'][-1]['cells'][-1] g.metadata['lights'].append({'owner': room['id'], 'point': lamp}) # Inlaid rugs add domestic color without reducing headroom or obstructing steps. ax, _, az = room['anchor'] for x in range(ax - 1, ax + 2): for z in range(az - 1, az + 2): if (x, z) in room['cells'] and full_support(g.get(x, room['floor'], z)) and is_air(g.get(x, room['floor'] + 1, z)): g.put(x, room['floor'], z, mat.accent + '_terracotta') PY cat > hearthwright/validate.py <<'PY' # Reusable diagnostics over final or reloaded geometry and explicit design metadata. from __future__ import annotations from collections import deque from dataclasses import dataclass, field, asdict from pathlib import Path from .blocks import Grid, AIR, registry, split_state, name, is_air, passable, seal, full_support, floor_support, blockstates_equivalent from .materials import DIRECTIONS from .navigation import flood, walkable @dataclass(frozen=True) class Diagnostic: """Identify a failed rule, responsible component and optional world coordinate.""" rule: str component: str message: str location: tuple | None = None @dataclass class Report: """Collect reusable validation results without raising until explicitly requested.""" diagnostics: list[Diagnostic] = field(default_factory=list) checked: dict = field(default_factory=dict) @property def valid(self): return not self.diagnostics def add(self, rule, component, message, location=None): self.diagnostics.append(Diagnostic(rule, component, message, tuple(location) if location is not None else None)) def require_valid(self): if not self.valid: details = '\n'.join(f'{d.rule}: {d.component} {d.location or ""}: {d.message}' for d in self.diagnostics[:25]) raise ValidationError(f'{len(self.diagnostics)} architectural validation failures\n{details}', self) return self def to_dict(self): return {'valid': self.valid, 'checked': self.checked, 'diagnostics': [asdict(d) for d in self.diagnostics]} class ValidationError(ValueError): """Carry the full report for programmatic error handling.""" def __init__(self, message, report): super().__init__(message) self.report = report RULES = [] def validation_rule(function): """Register an additional pure geometry rule receiving grid, metadata and report.""" if function not in RULES: RULES.append(function) return function def validate(building, metadata=None) -> Report: """Validate an artifact, sparse grid or reloaded path with explicit design metadata. Args: building: Building, Grid, or Path to a Litematic with a sidecar. metadata: Optional metadata dictionary or sidecar Path for reloaded geometry. Returns: Report: Diagnostics with component and coordinate information. """ if isinstance(building, (str, Path)): g = Grid.load(Path(building), metadata) else: g = building if isinstance(building, Grid) else building.grid m = g.metadata if metadata is None or isinstance(metadata, Path) else metadata report = Report() if m.get('schema') != 1 or any(k not in m for k in ('rooms', 'routes', 'start', 'interior')): report.add('metadata.schema', 'artifact', 'schema 1 architecture metadata with rooms, routes, start and interior is required') return report for rule in (states_and_bounds, multiblocks, attachments, circulation, envelope, furnishing, *RULES): rule(g, m, report) report.checked['blocks'] = len(g.blocks) return report def states_and_bounds(g, m, report): """Validate every explicit property and derived or recorded bounds.""" domains = registry() for state in sorted(set(g.blocks.values())): try: n, props = split_state(state) short = n.removeprefix('minecraft:') if short not in domains: raise ValueError('unknown block') if any(k not in domains[short][0] or v not in domains[short][0][k] for k, v in props.items()): raise ValueError('unknown property or value') except (TypeError, ValueError) as exc: point = next(p for p, s in g.blocks.items() if s == state) report.add('block.state', 'geometry', f'{state}: {exc}', point) if 'size_xyz' in m: ox, oy, oz = m['origin'] sx, sy, sz = m['size_xyz'] for p in g.blocks: if not (ox <= p[0] < ox + sx and oy <= p[1] < oy + sy and oz <= p[2] < oz + sz): report.add('bounds.export', 'geometry', 'block outside recorded schematic bounds', p) break b = g.bounds size = [b[i + 3] - b[i] + 3 for i in range(3)] limits = m.get('limits', {}) if limits and (max(size[0], size[2]) > limits['horizontal'] or size[1] > limits['vertical'] or size[0] * size[1] * size[2] > limits['volume'] or len(g.blocks) > limits['blocks']): report.add('bounds.resources', 'geometry', f'geometry size {size} exceeds configured Limits') report.checked['block_states'] = len(set(g.blocks.values())) def multiblocks(g, m, report): """Check both halves of doors and beds, as well as their actual support.""" count = 0 for (x, y, z), state in g.blocks.items(): n = name(state) if n.endswith('_door'): _, props = split_state(state) dy = 1 if props.get('half') == 'lower' else -1 other = g.get(x, y + dy, z) on, op = split_state(other) expected = dict(props, half='upper' if dy == 1 else 'lower') if on != 'minecraft:' + n or op != expected: report.add('multiblock.door', 'door', 'matching opposite door half is missing or inconsistent', (x, y, z)) if dy == 1 and not floor_support(g.get(x, y - 1, z)): report.add('support.door', 'door', 'door has no solid threshold', (x, y, z)) count += 1 elif n.endswith('_bed'): _, props = split_state(state) dx, dz = DIRECTIONS.get(props.get('facing'), (0, 0)) sign = 1 if props.get('part') == 'foot' else -1 other = g.get(x + sign * dx, y, z + sign * dz) on, op = split_state(other) if on != 'minecraft:' + n or op != dict(props, part='head' if sign == 1 else 'foot'): report.add('multiblock.bed', 'bed', 'adjacent matching bed head/foot is missing', (x, y, z)) if not full_support(g.get(x, y - 1, z)): report.add('support.bed', 'bed', 'bed is unsupported', (x, y, z)) count += 1 report.checked['multiblock_cells'] = count def attachments(g, m, report): """Check continuous supports, ladder backing, lantern suspension and plant substrate.""" for support in m['supports']: x, y0, z = support['bottom'] _, y1, _ = support['top'] for y in range(y0, y1 + 1): if not full_support(g.get(x, y, z)): report.add('support.column', support['owner'], 'load path to ground or waterbed is interrupted', (x, y, z)) break for (x, y, z), state in g.blocks.items(): n = name(state) if n == 'ladder': facing = split_state(state)[1].get('facing') dx, dz = DIRECTIONS.get(facing, (0, 0)) if not full_support(g.get(x - dx, y, z - dz)): report.add('support.ladder', 'ladder', 'rung lacks a solid backing wall', (x, y, z)) elif n == 'lantern': hanging = split_state(state)[1].get('hanging') == 'true' other = g.get(x, y + (1 if hanging else -1), z) if not full_support(other) and not name(other).endswith(('_fence', '_wall', '_slab')) and name(other) not in ('chain', 'lantern'): report.add('support.light', 'lantern', 'light has no supporting bracket or ceiling', (x, y, z)) elif n in ('poppy', 'cornflower', 'allium', 'oxeye_daisy', 'short_grass', 'fern', 'dandelion'): if name(g.get(x, y - 1, z)) not in ('grass_block', 'dirt', 'rooted_dirt', 'moss_block', 'podzol', 'coarse_dirt'): report.add('support.plant', 'planting', 'invalid substrate', (x, y, z)) elif n == 'wheat' and name(g.get(x, y - 1, z)) != 'farmland': report.add('support.crop', 'field', 'wheat needs farmland', (x, y, z)) elif n == 'lily_pad' and name(g.get(x, y - 1, z)) != 'water': report.add('support.lily', 'water', 'lily pad needs water', (x, y, z)) report.checked['support_columns'] = len(m['supports']) def circulation(g, m, report): """Inspect final clearance and traverse actual doors, stairs and ladders.""" for route in m['routes']: for x, y, z in route['points']: if any(not passable(g.get(x, y + dy, z)) for dy in range(route['headroom'])): report.add('clearance.route', route['owner'], 'reserved route has less than two clear headroom blocks', (x, y, z)) break if not walkable(g, (x, y, z)): report.add('support.route', route['owner'], 'route lacks a usable floor or ladder', (x, y, z)) break for stair in m['stairs']: state = g.get(*stair['point']) if not name(state).endswith('_stairs') or split_state(state)[1].get('facing') != stair['facing']: report.add('circulation.stair', stair['owner'], 'stair tread missing or faces against the ascent', stair['point']) x, y, z = stair['point'] if not full_support(g.get(x, y - 1, z)): report.add('support.stair', stair['owner'], 'tread lacks solid support', stair['point']) try: reachable = flood(g, m['start']) except ValueError as exc: report.add('circulation.limit', 'artifact', str(exc)) reachable = {} for room in m['rooms']: if tuple(room['anchor']) not in reachable: report.add('reachability.room', room['id'], 'room is unreachable from the exterior start', room['anchor']) if tuple(m['entrance']) not in reachable: report.add('reachability.entrance', 'entrance', 'the actual approach does not reach the door', m['entrance']) for f in m['features']: if tuple(f['target']) not in reachable: report.add('reachability.site', f['owner'], 'outdoor working space is unreachable', f['target']) for f in m['furniture']: if tuple(f['access']) not in reachable: report.add('reachability.furniture', f['owner'], f'{f["kind"]} interaction space is blocked', f['access']) report.checked['reachable_positions'] = len(reachable) report.checked['rooms'] = len(m['rooms']) def envelope(g, m, report): """Flood interior voids to detect leaks, including later damage to roofs and walls.""" interior = {tuple(p) for p in m['interior']} visited = set() for room in m['rooms']: start = tuple(room['anchor']) if start in visited or seal(g.get(*start)): continue queue = deque([start]) visited.add(start) failure = None while queue and failure is None: x, y, z = queue.popleft() for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)): q = x + dx, y + dy, z + dz if q in visited or seal(g.get(*q)): continue if q not in interior: failure = q break visited.add(q) queue.append(q) if failure is not None: report.add('envelope.leak', room['id'], 'interior air reaches outside its weather envelope', failure) for window in m['openings']: if window['kind'] != 'window': continue side = window['side'] dx, dz = DIRECTIONS[side] for x, y, z in window['cells']: if 'glass' not in name(g.get(x, y, z)): report.add('opening.glazing', window['owner'], 'glazed aperture was overwritten', (x, y, z)) break # A room-side full block obstructs light and view; roof cavities are exempt. q = x - dx, y, z - dz if q in interior and not passable(g.get(*q)): report.add('opening.clearance', window['owner'], 'window reveal is blocked inside the room', q) break for room in m['rooms']: for x, z in room['cells']: if name(g.get(x, room['floor'] + 1, z)) in ('water', 'dirt', 'grass_block', 'rooted_dirt', 'mud'): report.add('envelope.intrusion', room['id'], 'water or hillside material invades the usable room', (x, room['floor'] + 1, z)) break report.checked['envelope_voids'] = len(visited) def furnishing(g, m, report): """Require realized furniture, functional coverage and nearby light for every room.""" for room in m['rooms']: furnished = {f['function'] for f in m['furniture'] if f['owner'] == room['id']} for function in room['functions']: if function not in furnished: report.add('program.missing', room['id'], f'no realized furniture for {function}') floor = room['floor'] actual_lights = [record for record in m['lights'] if record['owner'] == room['id'] and name(g.get(*record['point'])) in ('lantern', 'glowstone', 'sea_lantern') and floor < record['point'][1] <= floor + 4] if not actual_lights: report.add('lighting.room', room['id'], 'room lacks an actual light near its occupied floor') for f in m['furniture']: for p, expected in zip(f['cells'], f['states']): if not blockstates_equivalent(g.get(*p), expected): report.add('furnishing.changed', f['owner'], f'{f["kind"]} was removed or overwritten', p) break report.checked['furniture_groups'] = len(m['furniture']) PY cat > hearthwright/__init__.py <<'PY' # Public compositional architecture, generation, transport and validation API. from .api import Choice, Span, Materials, Roof, Level, Join, Volume, Attachment, Feature, Site, Entrance, Limits, Design, DesignError from .build import Building, generate from .plan import resolve, Plan from .validate import validate, Report, Diagnostic, ValidationError, validation_rule from .blocks import blockstates_equivalent __all__ = ['Choice', 'Span', 'Materials', 'Roof', 'Level', 'Join', 'Volume', 'Attachment', 'Feature', 'Site', 'Entrance', 'Limits', 'Design', 'DesignError', 'Building', 'generate', 'resolve', 'Plan', 'validate', 'Report', 'Diagnostic', 'ValidationError', 'validation_rule', 'blockstates_equivalent'] PY cat > hearthwright/cli.py <<'PY' # Optional shared CLI transport; architectural clients also work through the Python API. import argparse from pathlib import Path from .build import generate DEFAULT_SEED = 20260925 def run(design, *, default_seed=DEFAULT_SEED): """Generate, validate, export and semantically verify a declared property.""" parser = argparse.ArgumentParser(description=design.name) parser.add_argument('--seed', type=int, default=default_seed) parser.add_argument('--output', type=Path, default=Path('output.litematic')) args = parser.parse_args() building = generate(design, args.seed) building.export(args.output) print(f'wrote {args.output}: {building.verify(args.output)}; validation passed') PY cat > pyproject.toml <<'TOML' [build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" [project] name = "hearthwright" version = "0.1.0" description = "Composable Minecraft architecture with geometry validation" requires-python = ">=3.11" [tool.setuptools.packages.find] include = ["hearthwright*"] [tool.pytest.ini_options] pythonpath = ["."] testpaths = ["test"] TOML /opt/venv/bin/python - <<'PY' from hearthwright import * b=generate(Design('test', (Volume('home'),)), 0, validate=False) r=b.validate() print(len(b.grid.blocks),len(r.diagnostics)) for d in r.diagnostics[:30]: print(d) PY

日志保留的输出末尾

2995 0
#021 · 2026-09-25T15:24:27.206Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/site.py' s=open(p).read(); a=s.index('def approach(ctx):'); b=s.index('\ndef feature(',a) s=s[:a]+'''def approach(ctx): """Connect a dry start to the threshold with supported directional stairs.""" g, p, mat = ctx.g, ctx.plan, ctx.plan.design.materials e = p.entrance x, z = e['point'] floor, side = e['floor'], e['side'] dx, dz = DIRECTIONS[side] first_tread = 1 for a in p.attachments: if a['to'] == e['volume'] and a['level'] == e['level'] and a['side'] == side and a['rect'].contains(x + dx, z + dz): first_tread = (a['rect'].depth if dz else a['rect'].width) + 1 ground_floor = p.design.site.water_depth + 1 if p.design.site.terrain == 'water' else 1 length = first_tread + max(0, floor - ground_floor) + 3 points = [] for d in range(1, length + 1): px, pz = x + dx * d, z + dz * d y = max(ground_floor, floor - max(0, d - first_tread)) stepped = d >= first_tread and y > ground_floor for w in (-1, 0, 1): xx, zz = px + (w if dz else 0), pz + (w if dx else 0) material = mat.floor + '_planks' if p.design.site.terrain == 'water' else 'stone_bricks' g.box((xx, 0, zz, xx, y, zz), material) if stepped: g.stair(xx, y, zz, mat.floor if p.design.site.terrain == 'water' else 'stone_brick', OPPOSITE[side]) g.metadata['stairs'].append({'owner': 'approach', 'point': (xx, y, zz), 'facing': OPPOSITE[side]}) # Clear the corridor through terrain and root buttresses before decoration. g.box((xx, y + 1, zz, xx, max(y + 2, ctx.ground.get((xx, zz), 1) + 2), zz), 'air') points.append((px, y + 1, pz)) g.route(points, 'approach') ctx.start = points[-1] g.metadata['start'] = ctx.start g.metadata['entrance'] = (x, floor + 1, z) for d in (2, length): px = x + dx * d + (3 if dz else 0) pz = z + dz * d + (3 if dx else 0) y = ctx.ground.get((px, pz), 1) if p.design.site.terrain == 'water': column(ctx, px, pz, ground_floor, 'approach-light') y = ground_floor if is_air(g.get(px, y + 1, pz)): g.put(px, y + 1, pz, 'stone_brick_wall') g.lantern(px, y + 2, pz, 'approach') ''' + s[b:]; open(p,'w').write(s) p='hearthwright/blocks.py';s=open(p).read();a=s.index(' for y, z, x in zip(*array.nonzero()):'); b=s.index(' for index, state',a);s=s[:a]+s[b:];open(p,'w').write(s) PY cat > examples/__init__.py <<'PY' # Independent, declarative client compositions; no geometry or validation implementations. PY cat > examples/stone_cottage.py <<'PY' # A compact stone dwelling with a small ladder loft and a sheltered entry. from hearthwright import Choice, Design, Level, Materials, Roof, Volume from hearthwright.cli import run DESIGN = Design( 'Alder cottage', volumes=(Volume('cottage', width=Choice(9, 11), depth=Choice(9, 11), levels=(Level(('living', 'kitchen', 'storage')), Level(('sleeping', 'study'), attic=True)), circulation='ladder', roof=Roof(ridge=Choice('x', 'z'), pitch=1.0)),), materials=Materials(wall='stone', roof='pale_stone', frame='oak', trim='spruce'), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/mediterranean_villa.py <<'PY' # Offset plaster volumes, turquoise shutters, open terraces and a garden pool. from hearthwright import Attachment, Choice, Design, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'House of the blue courtyard', volumes=( Volume('house', width=Choice(15, 17), depth=Choice(13, 15), levels=(Level(('living', 'kitchen')), Level(('sleeping', 'brewing'), layout=Choice('open', 'cross'))), roof=Roof(ridge='x', pitch=.5)), Volume('library', width=Choice(9, 11), depth=9, join=Join('house', 'east', Choice('front', 'back')), levels=(Level(('enchanting', 'study')),), roof=Roof(pitch=.5, ridge='z')), ), materials=Materials(wall='white', frame='birch', roof='tile', trim='sandstone', accent='cyan'), site=Site(planting='mediterranean'), attachments=(Attachment('terrace', 'house', depth=4, cover='pergola'), Attachment('balcony', 'house', level=1, side='west', width=7, cover='pergola')), features=(Feature('pool', 'south', Choice(7, 9)), Feature('garden', 'west', 5)), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/merchant_manor.py <<'PY' # Stone trading undercroft, upper entrance, dark gables and a connected study tower. from hearthwright import Attachment, Choice, Design, Entrance, Join, Level, Materials, Roof, Volume from hearthwright.cli import run DESIGN = Design( 'Rowan merchants house', volumes=( Volume('hall', width=Choice(17, 19), depth=Choice(15, 17), levels=(Level(('storage', 'trading'), wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross')), Level(('sleeping', 'study'), attic=True)), roof=Roof(pitch=1.0, ridge='x', dormers=Choice(1, 2))), Volume('tower', width=9, depth=9, join=Join('hall', 'north', Choice('front', 'back'), level=1), levels=(Level(('study',)), Level(('storage',)), Level(('study',))), circulation='ladder', roof=Roof(pitch=1.5, ridge='z')), ), materials=Materials(wall='plaster', frame='spruce', roof='slate', trim='spruce', accent='red'), entrance=Entrance('hall', level=1, bay=Choice('center', 'right')), attachments=(Attachment('terrace', 'hall', level=1, depth=3), Attachment('market', 'hall', side='east', width=7, depth=5, cover='canopy')), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/red_roof_farmstead.py <<'PY' # Red tiled farmhouse with a usable loft and a working farmyard. from hearthwright import Choice, Design, Feature, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Red ridge farm', volumes=(Volume('farmhouse', width=Choice(15, 17), depth=Choice(13, 15), levels=(Level(('living', 'kitchen', 'storage')), Level(('sleeping', 'workshop'), layout=Choice('open', 'cross'), attic=True)), roof=Roof(pitch=Choice(1.0, 1.5), ridge='x', dormers=1, cupola=True)),), materials=Materials(wall='timber', frame='spruce', roof='red', trim='sandstone', accent='red'), site=Site(margin=4, planting='sparse'), features=(Feature('well', 'west', 7), Feature('field', 'east', Choice(7, 9)), Feature('windmill', 'east', 9)), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/root_house.py <<'PY' # Inhabited organic mound, hollow polygonal rooms and spreading dark roots. from hearthwright import Choice, Design, Level, Materials, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Hollow elder home', volumes=(Volume('hollow', width=Choice(13, 15), depth=Choice(13, 15), levels=(Level(('living', 'kitchen', 'storage'), wall='earth'), Level(('sleeping', 'study'), wall='earth')), envelope='roots', roof=None, circulation=Choice('stairs', 'ladder'), root_spread=Choice(4, 5, 6), root_crown=Choice(4, 6)),), materials=Materials(wall='earth', frame='dark_oak', floor='spruce', accent='green'), site=Site(terrain='hill', margin=5, planting='temperate'), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/sandstone_cottage.py <<'PY' # Partly buried workshop, broad pale roof, entrance bay and waterside jetty. from hearthwright import Choice, Design, Entrance, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Willowbank house', volumes=( Volume('house', width=Choice(17, 19), depth=Choice(13, 15), levels=(Level(('workshop', 'storage'), wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross'))), roof=Roof(ridge='x', pitch=.5)), Volume('bay', width=9, depth=9, join=Join('house', 'west', Choice('front', 'back'), level=1), levels=(Level(('sleeping', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='plaster', frame='spruce', roof='sandstone', trim='dark_oak', accent='green'), entrance=Entrance('house', level=1, bay=Choice('center', 'right')), site=Site(terrain='slope', rise=5, planting='temperate'), features=(Feature('landing', 'south', 5),), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/stilt_water_cabin.py <<'PY' # Compact fishing quarters carried on piles over shallow water. from hearthwright import Attachment, Choice, Design, Feature, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Reedwater fishing cabin', volumes=(Volume('cabin', width=Choice(11, 13), depth=Choice(9, 11), raised=4, supports='posts', levels=(Level(('living', 'sleeping', 'fishing', 'kitchen')),), roof=Roof(ridge=Choice('x', 'z'), pitch=.5, eaves=2)),), materials=Materials(wall='timber', frame='oak', roof='dark_wood', trim='spruce', accent='green'), site=Site(terrain='water', water_depth=2, planting='wetland', margin=4), attachments=(Attachment('deck', 'cabin', side='south', depth=Choice(4, 5)),), features=(Feature('landing', 'west', 5),), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/stilted_manor.py <<'PY' # Recessed masonry base carrying an overhanging timber house and steep inhabited roof. from hearthwright import Attachment, Choice, Design, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Highbeam manor', volumes=(Volume('manor', width=Choice(19, 21), depth=Choice(17, 19), levels=(Level(('workshop', 'storage'), inset=2, wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross')), Level(('sleeping', 'study'), layout=Choice('open', 'cross'), attic=True)), roof=Roof(ridge='x', pitch=Choice(1.0, 1.5), dormers=2, cupola=True)),), materials=Materials(wall='plaster', frame='spruce', roof='gray', trim='spruce', accent='red'), site=Site(planting='conifer', margin=5), attachments=(Attachment('balcony', 'manor', level=1, side='east', width=7, depth=3),), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/canal_apothecary.py <<'PY' # A new composition: piled plaster quarters, a brewing wing and an open waterside pergola. from hearthwright import Attachment, Choice, Design, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Canal apothecary', volumes=( Volume('home', width=Choice(13, 15), depth=13, raised=3, supports='posts', levels=(Level(('living', 'kitchen', 'sleeping')),), roof=Roof(form='hip', pitch=.5)), Volume('laboratory', width=9, depth=Choice(9, 11), join=Join('home', 'east', Choice('front', 'back')), supports='posts', levels=(Level(('brewing', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='white', frame='birch', roof='tile', trim='sandstone', accent='cyan'), site=Site(terrain='water', planting='wetland'), attachments=(Attachment('deck', 'home', cover='pergola', depth=4),), features=(Feature('landing', 'west', 5),), ) if __name__ == '__main__': run(DESIGN) PY cat > examples/woodland_archive.py <<'PY' # A new composition: a small house, a tall archive wing, hipped roof and working garden. from hearthwright import Attachment, Choice, Design, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Woodland archive', volumes=( Volume('home', width=Choice(11, 13), depth=Choice(11, 13), levels=(Level(('living', 'kitchen', 'sleeping')),), roof=Roof(form=Choice('hip', 'gable'), ridge='x')), Volume('archive', width=9, depth=9, join=Join('home', Choice('east', 'west'), Choice('front', 'back')), levels=(Level(('study', 'enchanting')), Level(('study', 'storage'))), circulation='ladder', roof=Roof(pitch=1.5, ridge='z', cupola=False)), ), materials=Materials(wall='stone', frame='dark_oak', roof='red', trim='dark_oak', accent='blue'), site=Site(planting='temperate'), attachments=(Attachment('porch', 'home', width=7, depth=3, cover='pergola'),), features=(Feature('garden', 'south', Choice(5, 7)),), ) if __name__ == '__main__': run(DESIGN) PY cat > generate.py <<'PY' # Harness client: compose, validate and export Highbeam manor with an explicit seed. from examples.stilted_manor import DESIGN from hearthwright import blockstates_equivalent from hearthwright.cli import run if __name__ == '__main__': run(DESIGN) PY /opt/venv/bin/python - <<'PY' import importlib,time from hearthwright import generate names=['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive'] for n in names: t=time.time() try: b=generate(importlib.import_module('examples.'+n).DESIGN,0,validate=False) r=b.validate() print(n, len(b.grid.blocks),round(time.time()-t,2), 'errors',len(r.diagnostics),flush=True) for d in r.diagnostics[:12]:print(' ',d,flush=True) except Exception as e: print(n,type(e).__name__,str(e)[:1800],flush=True) PY

日志保留的输出末尾

ch', message='tread lacks solid support', location=(5, 6, 16)) Diagnostic(rule='circulation.stair', component='approach', message='stair tread missing or faces against the ascent', location=(6, 6, 16)) Diagnostic(rule='support.stair', component='approach', message='tread lacks solid support', location=(6, 6, 16)) Diagnostic(rule='circulation.stair', component='approach', message='stair tread missing or faces against the ascent', location=(6, 5, 17)) Diagnostic(rule='support.stair', component='approach', message='tread lacks solid support', location=(6, 5, 17)) Diagnostic(rule='circulation.stair', component='approach', message='stair tread missing or faces against the ascent', location=(6, 4, 18)) stilted_manor 7247 0.17 errors 7 Diagnostic(rule='support.column', component='manor/1', message='load path to ground or waterbed is interrupted', location=(10, 2, 18)) Diagnostic(rule='envelope.leak', component='manor/0/room0', message='interior air reaches outside its weather envelope', location=(4, 7, 8)) Diagnostic(rule='envelope.leak', component='manor/1/room0', message='interior air reaches outside its weather envelope', location=(4, 7, 8)) Diagnostic(rule='envelope.leak', component='manor/2/room0', message='interior air reaches outside its weather envelope', location=(2, 12, 6)) Diagnostic(rule='envelope.leak', component='manor/2/room1', message='interior air reaches outside its weather envelope', location=(15, 17, 0)) Diagnostic(rule='opening.clearance', component='manor/0', message='window reveal is blocked inside the room', location=(3, 4, 7)) Diagnostic(rule='opening.clearance', component='manor/1', message='window reveal is blocked inside the room', location=(1, 9, 6)) canal_apothecary DesignError laboratory: water buildings need posts and raised >= water_depth + 1 woodland_archive 4999 0.1 errors 1 Diagnostic(rule='envelope.leak', component='home/0/room0', message='interior air reaches outside its weather envelope', location=(2, 7, 8))
#022 · 2026-09-25T15:25:37.478Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/build.py';s=open(p).read().replace('for yy in range(y + 1, y + 4)', 'for yy in range(y, y + 4)') s=s.replace('(r.center[0], r.z0), (r.center[0], r.z1)', '(r.x0 + r.width // 3, r.z0), (r.x0 + r.width // 3, r.z1), (r.x1 - r.width // 3, r.z0), (r.x1 - r.width // 3, r.z1)') s=s.replace("for pt in points:\n g.put", "if any(not passable(g.get(x - dx, py, z - dz)) for _, py, _ in points):\n continue\n for pt in points:\n g.put") # Stair supports block wall-side windows; rooms remain lit and have other facades. s=s.replace('xx, y + 3, zz), log(mat.frame)', 'xx, y + 4, zz), log(mat.frame)').replace("g.put(xx, y + 3, zz, mat.floor + '_slab'", "g.put(xx, y + 4, zz, mat.floor + '_slab'").replace("g.lantern(xx, y + 4, zz, 'entrance')", "g.lantern(xx, y + 5, zz, 'entrance')") open(p,'w').write(s) p='hearthwright/site.py';s=open(p).read().replace('max(y + 2, ctx.ground', 'max(y + 3, ctx.ground') s=s.replace("g.put(r.x0, y + 1, r.z1, 'stone_brick_wall')", "g.put(r.x0, y + 1, r.z1, 'stone_bricks')") s=s.replace(" targets = [(f['target'], f['id']) for f in p.features]", " for route in g.metadata['routes']:\n forbidden |= {(x, z) for x, _, z in route['points']}\n forbidden.discard(start)\n targets = [(f['target'], f['id']) for f in p.features]") open(p,'w').write(s) p='hearthwright/plan.py';s=open(p).read().replace("if design.site.terrain == 'water' and (v.supports != 'posts' or v.raised < design.site.water_depth + 1):", "if design.site.terrain == 'water' and (v.supports != 'posts' or not v.join and v.raised < design.site.water_depth + 1):") open(p,'w').write(s) PY /opt/venv/bin/python - <<'PY' import importlib from hearthwright import generate names=['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive'] for n in names: try: b=generate(importlib.import_module('examples.'+n).DESIGN,0,validate=False);r=b.validate() print(n,len(r.diagnostics),flush=True) for d in r.diagnostics[:10]:print(' ',d,flush=True) except Exception as e:print(n,type(e).__name__,str(e)[:2000],flush=True) PY

日志保留的输出末尾

ad missing or faces against the ascent', location=(5, 6, 16)) Diagnostic(rule='support.stair', component='approach', message='tread lacks solid support', location=(5, 6, 16)) Diagnostic(rule='circulation.stair', component='approach', message='stair tread missing or faces against the ascent', location=(5, 5, 17)) Diagnostic(rule='support.stair', component='approach', message='tread lacks solid support', location=(5, 5, 17)) Diagnostic(rule='circulation.stair', component='approach', message='stair tread missing or faces against the ascent', location=(5, 4, 18)) stilted_manor 6 Diagnostic(rule='support.column', component='manor/1', message='load path to ground or waterbed is interrupted', location=(7, 2, 0)) Diagnostic(rule='support.column', component='manor/1', message='load path to ground or waterbed is interrupted', location=(7, 2, 18)) Diagnostic(rule='support.column', component='manor/1', message='load path to ground or waterbed is interrupted', location=(13, 2, 0)) Diagnostic(rule='support.column', component='manor/1', message='load path to ground or waterbed is interrupted', location=(13, 2, 18)) Diagnostic(rule='envelope.leak', component='manor/0/room0', message='interior air reaches outside its weather envelope', location=(2, 17, 0)) Diagnostic(rule='envelope.leak', component='manor/2/room1', message='interior air reaches outside its weather envelope', location=(15, 17, 0)) canal_apothecary 3 Diagnostic(rule='circulation.stair', component='approach', message='stair tread missing or faces against the ascent', location=(6, 5, 17)) Diagnostic(rule='support.stair', component='approach', message='tread lacks solid support', location=(6, 5, 17)) Diagnostic(rule='circulation.stair', component='approach', message='stair tread missing or faces against the ascent', location=(6, 4, 18)) woodland_archive 1 Diagnostic(rule='envelope.leak', component='home/0/room0', message='interior air reaches outside its weather envelope', location=(2, 7, 8))
#023 · 2026-09-25T15:26:25.569Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from hearthwright.build import Compiler from hearthwright.plan import resolve from hearthwright.navigation import flood,walkable from hearthwright.blocks import name from examples.merchant_manor import DESIGN c=Compiler(resolve(DESIGN,0)) try:c.construct() except Exception as e: print(e) g=c.g; seen=flood(g,c.start) print('start',c.start,'levels',[(s.id,s.floor,s.rect) for m in c.plan.masses for s in m.levels]) for r in g.metadata['routes']: if '->' in r['owner']: print(r['owner']) for p in r['points'][::2]:print(p,walkable(g,p),p in seen, [name(g.get(p[0],p[1]+dy,p[2])) for dy in (-1,0,1,2)]) from examples.mediterranean_villa import DESIGN from hearthwright import generate b=generate(DESIGN,0,validate=False) p=(18,7,10) print('villa leak',[(s.id,s.rect,s.floor) for m in b.plan.masses for s in m.levels]) for dx,dy,dz in ((0,0,0),(1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)): q=tuple(p[i]+(dx,dy,dz)[i] for i in range(3)); print(q,b.grid.get(*q)) PY

日志保留的输出末尾

0, z0=0, x1=18, z1=16)), ('hall/2', 12, Rect(x0=0, z0=0, x1=18, z1=16)), ('tower/0', 7, Rect(x0=10, z0=-8, x1=18, z1=0)), ('tower/1', 12, Rect(x0=10, z0=-8, x1=18, z1=0)), ('tower/2', 17, Rect(x0=10, z0=-8, x1=18, z1=0))] hall/0->hall/1 (1, 4, 2) True False ['spruce_stairs', 'air', 'air', 'air'] (1, 5, 3) True False ['spruce_stairs', 'air', 'air', 'air'] (1, 6, 4) True False ['spruce_stairs', 'air', 'air', 'spruce_planks'] (1, 7, 5) False False ['spruce_stairs', 'air', 'spruce_planks', 'spruce_planks'] (1, 8, 6) False False ['spruce_stairs', 'spruce_planks', 'spruce_planks', 'spruce_planks'] (1, 3, 1) True False ['oak_planks', 'air', 'air', 'air'] (1, 8, 7) True True ['oak_planks', 'air', 'air', 'air'] hall/1->hall/2 (1, 9, 2) True True ['spruce_stairs', 'air', 'air', 'air'] (1, 10, 3) True True ['spruce_stairs', 'air', 'air', 'air'] (1, 11, 4) True True ['spruce_stairs', 'air', 'air', 'air'] (1, 12, 5) True True ['spruce_stairs', 'air', 'air', 'air'] (1, 13, 6) True True ['spruce_stairs', 'air', 'air', 'air'] (1, 8, 1) True True ['oak_planks', 'air', 'air', 'air'] (1, 13, 7) True True ['oak_planks', 'air', 'air', 'air'] tower/0->tower/1 (11, 8, -4) True True ['oak_planks', 'ladder', 'ladder', 'ladder'] (11, 10, -4) True True ['ladder', 'ladder', 'ladder', 'ladder'] (11, 12, -4) True True ['ladder', 'ladder', 'ladder', 'ladder'] tower/1->tower/2 (11, 13, -4) True True ['ladder', 'ladder', 'ladder', 'ladder'] (11, 15, -4) True True ['ladder', 'ladder', 'ladder', 'ladder'] (11, 17, -4) True True ['ladder', 'ladder', 'ladder', 'air'] villa leak [('house/0', Rect(x0=0, z0=0, x1=16, z1=14), 2), ('house/1', Rect(x0=0, z0=0, x1=16, z1=14), 7), ('library/0', Rect(x0=16, z0=6, x1=26, z1=14), 2)] (18, 7, 10) minecraft:birch_trapdoor[facing=east,half=bottom,open=true,powered=false,waterlogged=false] (19, 7, 10) minecraft:oak_planks (17, 7, 10) minecraft:dirt (18, 8, 10) minecraft:bricks (18, 6, 10) minecraft:air (18, 7, 11) minecraft:oak_planks (18, 7, 9) minecraft:oak_planks
#024 · 2026-09-25T15:27:06.327Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/site.py';s=open(p).read().replace(" forbidden.discard(start)\n targets", " forbidden |= {(s['point'][0], s['point'][2]) for s in g.metadata['stairs']}\n forbidden.discard(start)\n targets");open(p,'w').write(s) p='hearthwright/roofs.py';s=open(p).read().replace(" if (x, y, z) not in ctx.interior:\n g.put(x, y, z, b)", " g.put(x, y, z, b)");open(p,'w').write(s) p='hearthwright/build.py';s=open(p).read().replace('for low, high in zip(mass.levels, mass.levels[1:]):', 'for flight, (low, high) in enumerate(zip(mass.levels, mass.levels[1:])):') s=s.replace(" x = max(low.rect.x0, high.rect.x0) + 1\n z0 = max(low.rect.z0, high.rect.z0) + 2", " candidates = [(x, z) for x, z in sorted(common) if all((xx, zz) in common for xx in (x, x + 1) for zz in range(z - 1, z + delta + 1))]\n if not candidates:\n raise DesignError(f'{owner}: no shared stair rectangle with both landings')\n x, z0 = min(candidates, key=lambda q: (-q[0] if flight % 2 else q[0], q[1]))") s=s.replace("if m.envelope != 'roots' and not any(a['floor']", "if m.envelope != 'roots' and all(is_air(g.get(x + d * dx, s.floor, z + d * dz)) for d in (1, 2)) and not any(a['floor']") open(p,'w').write(s) PY /opt/venv/bin/python - <<'PY' import importlib from hearthwright import generate names=['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive'] for n in names: try: b=generate(importlib.import_module('examples.'+n).DESIGN,0,validate=False);r=b.validate() print(n,len(r.diagnostics),flush=True) for d in r.diagnostics[:10]:print(' ',d,flush=True) except Exception as e:print(n,type(e).__name__,str(e)[:2000],flush=True) PY

日志保留的输出末尾

stone_cottage 0 mediterranean_villa 0 merchant_manor 3 Diagnostic(rule='support.plant', component='planting', message='invalid substrate', location=(19, 8, 11)) Diagnostic(rule='envelope.leak', component='hall/0/room0', message='interior air reaches outside its weather envelope', location=(16, 17, 1)) Diagnostic(rule='envelope.leak', component='hall/2/room0', message='interior air reaches outside its weather envelope', location=(9, 17, 1)) red_roof_farmstead 1 Diagnostic(rule='envelope.leak', component='farmhouse/0/room0', message='interior air reaches outside its weather envelope', location=(2, 12, 0)) root_house 0 sandstone_cottage 0 stilt_water_cabin 0 stilted_manor 3 Diagnostic(rule='envelope.leak', component='manor/0/room0', message='interior air reaches outside its weather envelope', location=(12, 7, 17)) Diagnostic(rule='envelope.leak', component='manor/2/room0', message='interior air reaches outside its weather envelope', location=(5, 17, 0)) Diagnostic(rule='envelope.leak', component='manor/2/room1', message='interior air reaches outside its weather envelope', location=(15, 17, 0)) canal_apothecary 0 woodland_archive 0
#025 · 2026-09-25T15:27:19.235Z · exit=0 · sed -n '100,135p' hearthwright/roofs.py && /opt/venv/bin/python - <<'PY'
sed -n '100,135p' hearthwright/roofs.py && /opt/venv/bin/python - <<'PY' from hearthwright import generate from examples.red_roof_farmstead import DESIGN b=generate(DESIGN,0,validate=False) p=(2,12,0) print(b.plan.masses[0].roof,[(s.floor,s.height,s.rect) for s in b.plan.masses[0].levels]) for x in (1,2,3): for z in (0,1,2): print(x,z,[(y,b.grid.get(x,y,z)) for y in (10,11,12,13,14)]) from examples.stilted_manor import DESIGN b=generate(DESIGN,0,validate=False) print('manor leak') p=(12,7,17) for dx,dy,dz in ((0,0,0),(1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)): q=tuple(p[i]+(dx,dy,dz)[i] for i in range(3)); print(q,b.grid.get(*q)) PY

日志保留的输出末尾

, (7, 3, Rect(x0=0, z0=0, x1=16, z1=14))] 1 0 [(10, 'minecraft:mangrove_planks'), (11, 'minecraft:mangrove_planks'), (12, 'minecraft:mangrove_stairs[facing=south,half=bottom,shape=straight,waterlogged=false]'), (13, 'minecraft:air'), (14, 'minecraft:air')] 1 1 [(10, 'minecraft:air'), (11, 'minecraft:air'), (12, 'minecraft:air'), (13, 'minecraft:mangrove_planks'), (14, 'minecraft:mangrove_stairs[facing=south,half=bottom,shape=straight,waterlogged=false]')] 1 2 [(10, 'minecraft:air'), (11, 'minecraft:air'), (12, 'minecraft:air'), (13, 'minecraft:air'), (14, 'minecraft:mangrove_planks')] 2 0 [(10, 'minecraft:mangrove_planks'), (11, 'minecraft:mangrove_planks'), (12, 'minecraft:mangrove_stairs[facing=south,half=bottom,shape=straight,waterlogged=false]'), (13, 'minecraft:air'), (14, 'minecraft:air')] 2 1 [(10, 'minecraft:air'), (11, 'minecraft:air'), (12, 'minecraft:air'), (13, 'minecraft:mangrove_planks'), (14, 'minecraft:mangrove_stairs[facing=south,half=bottom,shape=straight,waterlogged=false]')] 2 2 [(10, 'minecraft:air'), (11, 'minecraft:air'), (12, 'minecraft:air'), (13, 'minecraft:air'), (14, 'minecraft:mangrove_planks')] 3 0 [(10, 'minecraft:mangrove_planks'), (11, 'minecraft:mangrove_planks'), (12, 'minecraft:mangrove_stairs[facing=south,half=bottom,shape=straight,waterlogged=false]'), (13, 'minecraft:air'), (14, 'minecraft:air')] 3 1 [(10, 'minecraft:air'), (11, 'minecraft:air'), (12, 'minecraft:air'), (13, 'minecraft:mangrove_planks'), (14, 'minecraft:mangrove_stairs[facing=south,half=bottom,shape=straight,waterlogged=false]')] 3 2 [(10, 'minecraft:air'), (11, 'minecraft:air'), (12, 'minecraft:air'), (13, 'minecraft:air'), (14, 'minecraft:mangrove_planks')] manor leak (12, 7, 17) minecraft:lantern[hanging=false,waterlogged=false] (13, 7, 17) minecraft:oak_planks (11, 7, 17) minecraft:oak_planks (12, 8, 17) minecraft:air (12, 6, 17) minecraft:oak_slab[type=top,waterlogged=false] (12, 7, 18) minecraft:stripped_spruce_log[axis=y] (12, 7, 16) minecraft:oak_planks
#026 · 2026-09-25T15:27:23.268Z · exit=0 · sed -n '77,103p' hearthwright/roofs.py && sed -n '278,315p' hearthwright/build.py
sed -n '77,103p' hearthwright/roofs.py && sed -n '278,315p' hearthwright/build.py

日志保留的输出末尾

+ abs(p[2] - cz), p)) room['anchor'] = anchor g.route(path_to(parents, anchor), room['id']) g.metadata['rooms'].append({**room, 'cells': sorted(room['cells'])}) for f in g.metadata['features']: if tuple(f['target']) not in parents: raise DesignError(f'{f["owner"]}: outdoor target is not reachable') g.route(path_to(parents, f['target']), f['owner']) def connect_fences(self): """Resolve explicit fence connections so exports render and paste consistently.""" g = self.g for (x, y, z), state in list(g.blocks.items()): if name(state).endswith('_fence'): properties = {} for face, (dx, dz) in DIRECTIONS.items(): n = name(g.get(x + dx, y, z + dz)) properties[face] = str(n.endswith(('_fence', '_log', '_planks', '_wood'))).lower() g.put(x, y, z, name(state), **properties) def geometry_signature(self): """Digest actual structural cells, excluding texture, plants and cosmetic materials.""" # Geometry classifications retain roof/stair topology, floor masks and partitions. selected = [] for mass in self.plan.masses: r = mass.rect.inset(-2) bottom = mass.levels[0].floor for (x, y, z), state in sorted(self.g.blocks.items()): if not r.contains(x, z) or y < bottom: continue n = name(state) role = 'door' if n.endswith('_door') else 'glass' if 'glass' in n else 'stair' if n.endswith('_stairs') else 'ladder' if n == 'ladder' else 'solid' if n.endswith(('_planks', '_log', '_stem')) or n in ('stone_bricks', 'andesite', 'stone', 'cobblestone', 'calcite', 'smooth_quartz', 'smooth_sandstone', 'deepslate_tiles', 'mangrove_planks', 'bricks', 'light_blue_concrete', 'light_blue_terracotta', 'tuff', 'mossy_cobblestone', 'white_terracotta') else None
#027 · 2026-09-25T15:28:00.356Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/roofs.py';s=open(p).read().replace(" if (x, y, z) not in ctx.interior:\n g.put(x, y, z, b)", " g.put(x, y, z, b)");open(p,'w').write(s) p='hearthwright/build.py';s=open(p).read().replace("a['floor'] == s.floor and a['rect'].contains(x + dx, z + dz)", "a['floor'] <= s.floor <= a['floor'] + (6 if a['cover'] != 'open' else 1) and a['rect'].contains(x + dx, z + dz)") s=s.replace('xx, y + 4, zz), log(mat.frame)', 'xx, y + 3, zz), log(mat.frame)').replace("g.put(xx, y + 4, zz, mat.floor + '_slab'", "g.put(xx, y + 3, zz, mat.floor + '_slab'").replace("g.lantern(xx, y + 5, zz, 'entrance')", "g.lantern(xx, y + 4, zz, 'entrance')") open(p,'w').write(s) p='hearthwright/site.py';s=open(p).read().replace('max(y + 3, ctx.ground.get((xx, zz), 1) + 2)', 'max(y + (3 if d > first_tread else 2), ctx.ground.get((xx, zz), 1) + 2)');open(p,'w').write(s) PY /opt/venv/bin/python - <<'PY' import importlib from hearthwright import generate names=['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive'] for n in names: try: b=generate(importlib.import_module('examples.'+n).DESIGN,0,validate=False);r=b.validate() print(n,len(r.diagnostics),flush=True) for d in r.diagnostics[:8]:print(' ',d,flush=True) except Exception as e:print(n,type(e).__name__,str(e)[:2000],flush=True) PY

日志保留的输出末尾

stone_cottage 0 mediterranean_villa 0 merchant_manor 2 Diagnostic(rule='envelope.leak', component='hall/0/room0', message='interior air reaches outside its weather envelope', location=(16, 17, 1)) Diagnostic(rule='envelope.leak', component='hall/2/room0', message='interior air reaches outside its weather envelope', location=(9, 17, 1)) red_roof_farmstead 0 root_house 0 sandstone_cottage 0 stilt_water_cabin 0 stilted_manor 0 canal_apothecary 0 woodland_archive 0
#028 · 2026-09-25T15:28:30.288Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/roofs.py';s=open(p).read().replace(" low = max(h - 2, min(neighbor, default=h))", " covered = [surface_map[x, z]['height'] for surface_map in maps.values() if (x, z) in surface_map]\n low = min(neighbor + covered, default=h)\n owner = next(m for m in plan.masses if m.name == v['owner'])\n if (x, z) in owner.levels[-1].cells:\n low = max(low, owner.levels[-1].floor + owner.levels[-1].height)") open(p,'w').write(s) PY /opt/venv/bin/python - <<'PY' from hearthwright import generate from examples.merchant_manor import DESIGN b=generate(DESIGN,0,validate=False);print(b.validate().to_dict()) PY

日志保留的输出末尾

{'valid': True, 'checked': {'block_states': 83, 'multiblock_cells': 10, 'support_columns': 8, 'reachable_positions': 2424, 'rooms': 6, 'envelope_voids': 4234, 'furniture_groups': 20, 'blocks': 9244}, 'diagnostics': []}
#029 · 2026-09-25T15:28:55.525Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' import importlib,time,json from hearthwright import generate names=['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive'] seeds=[0,1,2,3,4,5,17,101,20260925,-1,2**63-1,-2**63] fail=[] for n in names: signatures=set() for seed in seeds: try: b=generate(importlib.import_module('examples.'+n).DESIGN,seed) signatures.add(b.metadata['structural_signature']) except Exception as e: fail.append((n,seed,str(e))); print('FAIL',n,seed,str(e)[:1000],flush=True) print(n, 'completed; observed structures',len(signatures),flush=True) print('failures',len(fail),flush=True) open('/tmp/hearthwright_failures.json','w').write(json.dumps(fail,indent=2)) PY

日志保留的输出末尾

itectural validation failures opening.clearance: farmhouse/1 (7, 11, 11): window reveal is blocked inside the room FAIL red_roof_farmstead 3 1 architectural validation failures opening.clearance: farmhouse/1 (7, 11, 11): window reveal is blocked inside the room FAIL red_roof_farmstead 17 1 architectural validation failures opening.clearance: farmhouse/1 (8, 11, 13): window reveal is blocked inside the room FAIL red_roof_farmstead 101 1 architectural validation failures opening.clearance: farmhouse/1 (7, 11, 13): window reveal is blocked inside the room FAIL red_roof_farmstead -1 1 architectural validation failures opening.clearance: farmhouse/1 (7, 11, 11): window reveal is blocked inside the room FAIL red_roof_farmstead 9223372036854775807 1 architectural validation failures opening.clearance: farmhouse/1 (8, 11, 11): window reveal is blocked inside the room red_roof_farmstead completed; observed structures 6 root_house completed; observed structures 11 FAIL sandstone_cottage 1 bay/0/room0: no reachable floor; review its circulation or join FAIL sandstone_cottage 101 bay/0/room0: no reachable floor; review its circulation or join FAIL sandstone_cottage -1 bay/0/room0: no reachable floor; review its circulation or join FAIL sandstone_cottage -9223372036854775808 bay/0/room0: no reachable floor; review its circulation or join sandstone_cottage completed; observed structures 8 stilt_water_cabin completed; observed structures 10 stilted_manor completed; observed structures 11 canal_apothecary completed; observed structures 8 FAIL woodland_archive 1 archive/1/room0: no reachable floor; review its circulation or join FAIL woodland_archive 101 archive/1/room0: no reachable floor; review its circulation or join FAIL woodland_archive -1 archive/1/room0: no reachable floor; review its circulation or join FAIL woodland_archive -9223372036854775808 archive/1/room0: no reachable floor; review its circulation or join woodland_archive completed; observed structures 8 failures 14
#030 · 2026-09-25T15:29:02.379Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' import importlib from pathlib import Path from hearthwright import generate for n in ['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive']: try: b=generate(importlib.import_module('examples.'+n).DESIGN,0) path=Path('samples')/(n+'-0.litematic') b.export(path); print(n,b.verify(path),flush=True) except Exception as e:print('FAIL',n,str(e)[:1200],flush=True) PY

日志保留的输出末尾

stone_cottage {'blocks': 3137, 'states': 52, 'size_xyz': [27, 19, 39]} mediterranean_villa {'blocks': 7564, 'states': 85, 'size_xyz': [53, 19, 45]} merchant_manor {'blocks': 9244, 'states': 83, 'size_xyz': [38, 32, 58]} red_roof_farmstead {'blocks': 6791, 'states': 70, 'size_xyz': [53, 33, 41]} root_house {'blocks': 7100, 'states': 41, 'size_xyz': [37, 25, 49]} sandstone_cottage {'blocks': 12790, 'states': 69, 'size_xyz': [43, 20, 48]} stilt_water_cabin {'blocks': 5256, 'states': 58, 'size_xyz': [35, 17, 41]} stilted_manor {'blocks': 7685, 'states': 71, 'size_xyz': [38, 41, 45]} canal_apothecary {'blocks': 7770, 'states': 72, 'size_xyz': [47, 18, 44]} woodland_archive {'blocks': 5028, 'states': 66, 'size_xyz': [37, 22, 41]}
#031 · 2026-09-25T15:29:26.994Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from hearthwright import generate from examples.red_roof_farmstead import DESIGN b=generate(DESIGN,1,validate=False) p=(7,11,11);print('blocked dormer',b.grid.get(*p)) from hearthwright.build import Compiler from hearthwright.plan import resolve from hearthwright.navigation import flood,walkable from examples.sandstone_cottage import DESIGN for spec in [DESIGN,__import__('examples.woodland_archive',fromlist=['DESIGN']).DESIGN]: c=Compiler(resolve(spec,1)) try:c.construct() except Exception as e: print(e) print([(m.name,m.join,m.circulation,m.rect) for m in c.plan.masses]) seen=flood(c.g,c.start) for route in c.g.metadata['routes']: if 'join' in route['owner'] or '->' in route['owner']: print(route['owner'],[(p,p in seen,walkable(c.g,p),[c.g.get(p[0],p[1]+dy,p[2]) for dy in (0,1)]) for p in route['points']]) PY

日志保留的输出末尾

['minecraft:air', 'minecraft:air']), ((1, 8, 6), True, True, ['minecraft:air', 'minecraft:air']), ((2, 8, 6), True, True, ['minecraft:air', 'minecraft:air']), ((1, 3, 1), True, True, ['minecraft:air', 'minecraft:air']), ((2, 3, 1), True, True, ['minecraft:air', 'minecraft:air']), ((1, 8, 7), True, True, ['minecraft:air', 'minecraft:air']), ((2, 8, 7), True, True, ['minecraft:air', 'minecraft:air'])] bay/join [((1, 8, 4), False, True, ['minecraft:air', 'minecraft:air']), ((0, 8, 4), False, True, ['minecraft:spruce_door[facing=west,half=lower,hinge=left,open=false,powered=false]', 'minecraft:spruce_door[facing=west,half=upper,hinge=left,open=false,powered=false]']), ((-1, 8, 4), False, True, ['minecraft:air', 'minecraft:air'])] archive/1/room0: no reachable floor; review its circulation or join [('home', None, 'stairs', Rect(x0=0, z0=0, x1=10, z1=10)), ('archive', {'to': 'home', 'level': 0, 'side': 'east', 'point': (10, 6), 'floor': 2}, 'ladder', Rect(x0=10, z0=2, x1=18, z1=10))] archive/0->archive/1 [((11, 3, 6), True, True, ['minecraft:air', 'minecraft:air']), ((11, 4, 6), False, False, ['minecraft:air', 'minecraft:ladder[facing=east,waterlogged=false]']), ((11, 5, 6), False, True, ['minecraft:ladder[facing=east,waterlogged=false]', 'minecraft:ladder[facing=east,waterlogged=false]']), ((11, 6, 6), False, True, ['minecraft:ladder[facing=east,waterlogged=false]', 'minecraft:ladder[facing=east,waterlogged=false]']), ((11, 7, 6), False, True, ['minecraft:ladder[facing=east,waterlogged=false]', 'minecraft:ladder[facing=east,waterlogged=false]']), ((11, 8, 6), False, True, ['minecraft:ladder[facing=east,waterlogged=false]', 'minecraft:air'])] archive/join [((9, 3, 6), True, True, ['minecraft:air', 'minecraft:air']), ((10, 3, 6), True, True, ['minecraft:spruce_door[facing=east,half=lower,hinge=left,open=false,powered=false]', 'minecraft:spruce_door[facing=east,half=upper,hinge=left,open=false,powered=false]']), ((11, 3, 6), True, True, ['minecraft:air', 'minecraft:air'])]
#032 · 2026-09-25T15:29:28.700Z · exit=0 · xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- samples/stil
xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- samples/stilted_manor-0.litematic previews/stilted_manor-0

日志保留的输出末尾

not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! Using WebGL2 context for WebGL requests instead of WebGL Loaded structure from /work/generator/workspace/samples/stilted_manor-0.litematic in 24 ms Set structure in renderer in 245 ms Rendering stilted_manor-0 at azimuth 0 (1/1)... Rendered image in 83 ms (azimuth: 3.6651914291880923, elevation: 0.3141592653589793) wrote /work/generator/workspace/previews/stilted_manor-0/front.png (512x512, azimuth 210, elevation 18) Loaded structure from /work/generator/workspace/samples/stilted_manor-0.litematic in 23 ms Set structure in renderer in 151 ms Rendering stilted_manor-0 at azimuth 0 (1/1)... Rendered image in 23 ms (azimuth: 0.6108652381980153, elevation: 0.3839724354387525) wrote /work/generator/workspace/previews/stilted_manor-0/rear.png (512x512, azimuth 35, elevation 22) Loaded structure from /work/generator/workspace/samples/stilted_manor-0.litematic in 29 ms Set structure in renderer in 140 ms Rendering stilted_manor-0 at azimuth 0 (1/1)... Rendered image in 19 ms (azimuth: 2.0943951023931953, elevation: 0.24434609527920614) wrote /work/generator/workspace/previews/stilted_manor-0/side.png (512x512, azimuth 120, elevation 14) Loaded structure from /work/generator/workspace/samples/stilted_manor-0.litematic in 29 ms Set structure in renderer in 131 ms Rendering stilted_manor-0 at azimuth 0 (1/1)... Rendered image in 26 ms (azimuth: 3.6651914291880923, elevation: 1.0821041362364843) wrote /work/generator/workspace/previews/stilted_manor-0/top.png (512x512, azimuth 210, elevation 62)
#033 · 2026-09-25T15:30:17.397Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/roofs.py';s=open(p).read().replace('for d in (-1, 0, 1) for y in (1, 2)', 'for d in (-1, 1) for y in (1, 2)');open(p,'w').write(s) p='hearthwright/build.py';s=open(p).read();needle=" delta = high.floor - low.floor\n if mass.circulation" replacement=""" delta = high.floor - low.floor protected = set() openings = [m.join for m in self.plan.masses if m.join] openings.append(self.plan.entrance) openings += [{'point': a['door'], 'floor': a['floor'], 'side': a['side']} for a in self.plan.attachments] for opening in openings: if opening['floor'] not in (low.floor, high.floor): continue px, pz = opening['point'] dx, dz = DIRECTIONS[opening['side']] protected.update((px + dx * d, pz + dz * d) for d in (-1, 0, 1)) if mass.circulation""" s=s.replace(needle,replacement) s=s.replace("if all((x, yy, z) in self.interior for yy", "if (x, z) not in protected and all((x, yy, z) in self.interior for yy") s=s.replace("if all((xx, zz) in common for xx in (x, x + 1) for zz in range(z - 1, z + delta + 1))", "if all((xx, zz) in common and (xx, zz) not in protected for xx in (x, x + 1) for zz in range(z - 1, z + delta + 1))") open(p,'w').write(s) p='hearthwright/plan.py';s=open(p).read().replace(" length = entrance['floor'] + 10", " landing = design.site.water_depth + 1 if design.site.terrain == 'water' else 1\n extension = max([a['rect'].depth if side in ('north', 'south') else a['rect'].width for a in attachments if a['to'] == entrance['volume'] and a['level'] == entrance['level'] and a['side'] == side] + [0])\n length = max(0, entrance['floor'] - landing) + extension + 4") open(p,'w').write(s) PY /opt/venv/bin/python - <<'PY' import importlib from hearthwright import generate for n in ['red_roof_farmstead','sandstone_cottage','woodland_archive']: for seed in (0,1,3,17,101,-1): try: b=generate(importlib.import_module('examples.'+n).DESIGN,seed);print(n,seed,'OK') except Exception as e:print('FAIL',n,seed,str(e)[:2000]) PY

日志保留的输出末尾

red_roof_farmstead 0 OK red_roof_farmstead 1 OK red_roof_farmstead 3 OK red_roof_farmstead 17 OK red_roof_farmstead 101 OK red_roof_farmstead -1 OK sandstone_cottage 0 OK sandstone_cottage 1 OK sandstone_cottage 3 OK sandstone_cottage 17 OK sandstone_cottage 101 OK sandstone_cottage -1 OK woodland_archive 0 OK woodland_archive 1 OK woodland_archive 3 OK woodland_archive 17 OK woodland_archive 101 OK woodland_archive -1 OK
#034 · 2026-09-25T15:33:16.627Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/materials.py';s=open(p).read().replace("'plaster': ('calcite', 'calcite', 'smooth_sandstone', 'white_terracotta')", "'plaster': ('calcite',) * 12 + ('smooth_sandstone',)");open(p,'w').write(s) p='hearthwright/plan.py';s=open(p).read().replace('from dataclasses import asdict, dataclass, field','from dataclasses import asdict, dataclass, field, replace').replace('from .api import Design, DesignError, choose, document, domain', 'from .api import Choice, Design, DesignError, choose, document, domain') s=s.replace(' for v in design.volumes:\n if v.name', " for v in design.volumes:\n if isinstance(v.levels, Choice):\n selected = choose(v.levels, rng, lambda levels: isinstance(levels, tuple) and 1 <= len(levels) <= (2 if v.envelope == 'roots' else 4), f'{v.name}.levels')\n v = replace(v, levels=selected)\n if v.name") open(p,'w').write(s) p='hearthwright/site.py';s=open(p).read().replace('max(0, z - main.z0 + 2)', 'max(0, main.z1 - z + 2)') needle=" if p.design.site.terrain == 'water':\n y = p.design.site.water_depth + 1\n if kind in ('landing',):" repl=""" if p.design.site.terrain == 'water': y = p.design.site.water_depth + 1 elif kind == 'landing': # A river reach is excavated around the jetty, with a continuous gravel bed. y = 2 for x, z in sorted(r.inset(-2).cells()): g.box((x, 1, z, x, max(3, ctx.ground.get((x, z), 1) + 2), z), 'air') g.put(x, 0, z, 'gravel') g.put(x, 1, z, 'water', level='0') ctx.ground[x, z] = 0 if kind in ('landing',):""" s=s.replace(needle,repl).replace(" g.box((x, 1, z, x, y - 1, z), 'dirt' if kind != 'landing' else mat.floor + '_planks')", " if kind != 'landing':\n g.box((x, 1, z, x, y - 1, z), 'dirt')") # Genuinely narrow and tiered conifers. s=s.replace("height = g.rng.randint(7, 10) if policy == 'mediterranean' else g.rng.randint(5, 8)", "height = g.rng.randint(9, 12) if policy == 'conifer' else g.rng.randint(7, 10) if policy == 'mediterranean' else g.rng.randint(5, 8)") s=s.replace(" else:\n for dy in range(-3, 2):", " elif policy == 'conifer':\n for level in range(3, height + 2):\n radius = max(0, (height - level) // 4 + (1 if level % 3 == 0 else 0))\n for dx in range(-radius, radius + 1):\n for dz in range(-radius, radius + 1):\n if abs(dx) + abs(dz) <= radius + 1:\n leaves.add((x + dx, base + level, z + dz))\n else:\n for dy in range(-3, 2):") open(p,'w').write(s) p='hearthwright/roofs.py';s=open(p).read().replace("g.box((x, bottom, z, x, base, z), mat.floor + '_planks')", "g.box((x, bottom, z, x, base, z), log(mat.frame) if x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) else 'calcite', **({'axis': 'y'} if x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) else {}))") # Add gable mullions and eave corbels without occupying rooms. needle=" ctx.roof_maps = maps\n for mass in plan.masses:" repl=""" ctx.roof_maps = maps for mass in plan.masses: if not mass.roof: continue s, roof = mass.levels[-1], mass.roof if roof['form'] == 'gable': # Paired upper gable lights fit under the actual backed slope. axis_x = roof['ridge'] == 'x' for end in (s.rect.x0, s.rect.x1) if axis_x else (s.rect.z0, s.rect.z1): for offset in (-2, 2): x, z = (end, s.rect.center[1] + offset) if axis_x else (s.rect.center[0] + offset, end) top = maps[mass.name][x, z]['height'] - 2 bottom = max(s.floor + s.height + 1, top - 2) if top >= bottom: for y in range(bottom, top + 1): g.put(x, y, z, 'light_gray_stained_glass') # Corbels attach the thick eaves to the top wall beam. for x, z in sorted(s.cells - ctx.inner[s.id]): if (x + z) % 4: continue for side, (dx, dz) in DIRECTIONS.items(): if (x + dx, z + dz) in s.cells: continue y = s.floor + s.height - 1 if is_air(g.get(x + dx, y, z + dz)): g.stair(x + dx, y, z + dz, mat.floor, {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'}[side], 'top') for mass in plan.masses:""" s=s.replace(needle,repl);open(p,'w').write(s) PY cat > examples/orchard_house.py <<'PY' # A new design space with a sampled floor count, domestic loft and covered working terrace. from hearthwright import Attachment, Choice, Design, Feature, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Orchard keeper house', volumes=(Volume('home', width=Choice(13, 15), depth=Choice(13, 15), levels=Choice((Level(('living', 'kitchen', 'sleeping', 'storage')),), (Level(('living', 'kitchen', 'workshop')), Level(('sleeping', 'storage'), attic=True))), roof=Roof(form=Choice('gable', 'hip'), ridge=Choice('x', 'z'), pitch=1.0)),), materials=Materials(wall='plaster', frame='dark_oak', roof='sandstone', trim='dark_oak', accent='green'), site=Site(planting='temperate', margin=4), attachments=(Attachment('terrace', 'home', side='east', width=7, cover='pergola'),), features=(Feature('field', 'west', Choice(7, 9)), Feature('well', 'south', 7)), ) if __name__ == '__main__': run(DESIGN) PY /opt/venv/bin/python - <<'PY' import importlib,json from hearthwright import generate names=['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive','orchard_house'] seeds=[0,1,2,3,4,5,17,101,20260925,-1,2**63-1,-2**63] fail=[] for n in names: signatures=set() for seed in seeds: try: b=generate(importlib.import_module('examples.'+n).DESIGN,seed) signatures.add(b.metadata['structural_signature']) except Exception as e: fail.append((n,seed,str(e))); print('FAIL',n,seed,str(e)[:1000],flush=True) print(n,'structures',len(signatures),flush=True) print('FAILURES',len(fail));open('/tmp/hearthwright_failures.json','w').write(json.dumps(fail,indent=2)) PY

日志保留的输出末尾

stone_cottage structures 8 mediterranean_villa structures 11 merchant_manor structures 11 red_roof_farmstead structures 11 root_house structures 11 sandstone_cottage structures 11 stilt_water_cabin structures 10 stilted_manor structures 11 canal_apothecary structures 8 woodland_archive structures 11 orchard_house structures 11 FAILURES 0
#035 · 2026-09-25T15:33:48.921Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' import importlib from pathlib import Path from hearthwright import generate for n in ['stone_cottage','mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','canal_apothecary','woodland_archive','orchard_house']: try: b=generate(importlib.import_module('examples.'+n).DESIGN,0) b.export(Path('samples')/(n+'-0.litematic')) except Exception as e: print(n,str(e)[:1200]) PY

日志保留的输出末尾

#036 · 2026-09-25T15:35:04.315Z · exit=0 · mkdir -p scripts && cat > scripts/sample_batch.py <<'PY'
mkdir -p scripts && cat > scripts/sample_batch.py <<'PY' # Reproduce the finite sample corpus and its actual-geometry validation manifest. import argparse import importlib import json from pathlib import Path from hearthwright import Building, generate CLIENTS = ( 'mediterranean_villa', 'merchant_manor', 'red_roof_farmstead', 'root_house', 'sandstone_cottage', 'stilt_water_cabin', 'stilted_manor', 'stone_cottage', 'canal_apothecary', 'woodland_archive', 'orchard_house', ) def main(): """Generate only a finite declared corpus, never enumerate parameter products.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--seeds', type=int, nargs='+', default=[0, 1]) parser.add_argument('--output-dir', type=Path, default=Path('samples')) args = parser.parse_args() records = [] for client in CLIENTS: design = importlib.import_module('examples.' + client).DESIGN for seed in args.seeds: building = generate(design, seed) path = args.output_dir / f'{client}-{seed}.litematic' building.export(path) roundtrip = building.verify(path) report = Building.load(path).validate().require_valid() records.append({'client': f'examples/{client}.py', 'seed': seed, 'schematic': path.name, 'metadata': path.with_suffix('.json').name, 'resolved': building.plan.summary(), 'structural_signature': building.metadata['structural_signature'], 'geometry_signature': building.metadata['geometry_signature'], 'validation': report.to_dict(), 'roundtrip': roundtrip}) print(f'{client} seed={seed}: {roundtrip["blocks"]} blocks, validated after reload', flush=True) manifest = {'schema': 1, 'command': 'python -m scripts.sample_batch', 'samples': records, 'observed_structures': len({r['structural_signature'] for r in records}), 'observed_geometry_signatures': len({r['geometry_signature'] for r in records})} args.output_dir.mkdir(parents=True, exist_ok=True) (args.output_dir / 'manifest.json').write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n') if __name__ == '__main__': main() PY cat > scripts/__init__.py <<'PY' # Reproducible sample and review commands. PY cat > scripts/render_batch.py <<'PY' # Render a representative spread, including explicit furnished floor cutaways. import argparse import json from pathlib import Path import subprocess ROOT = Path(__file__).resolve().parents[1] PYTHON = Path('/opt/venv/bin/python') RENDERER = Path('/work/generator/MCRender/node_modules/.bin/vite-node') CUTTER = Path('/opt/tools/cutaway.py') def main(): """Render both exterior sides and floor cutaways of every client in the corpus.""" parser = argparse.ArgumentParser() parser.add_argument('--manifest', type=Path, default=ROOT / 'samples/manifest.json') parser.add_argument('--all-seeds', action='store_true') args = parser.parse_args() manifest = json.loads(args.manifest.read_text()) jobs = [] selected = set() for sample in manifest['samples']: client = Path(sample['client']).stem if client in selected and not args.all_seeds: continue selected.add(client) key = f'{client}-{sample["seed"]}' source = args.manifest.parent / sample['schematic'] folder = ROOT / 'previews' / key folder.mkdir(parents=True, exist_ok=True) jobs.append({'input': str(source.resolve()), 'folder': str(folder), 'name': key, 'views': ['front', 'rear']}) levels = sample['resolved']['masses'][0]['levels'] for i, level in enumerate(levels): path = folder / f'floor-{i}.litematic' height = level['floor'] + 3 subprocess.run([str(PYTHON), str(CUTTER), '--input', str(source), '--output', str(path), '--max-y', str(height)], check=True, stdout=subprocess.DEVNULL) jobs.append({'input': str(path), 'folder': str(folder), 'name': f'{key} floor {i}', 'views': [f'floor-{i}']}) jobs_path = ROOT / 'previews/render_jobs.json' jobs_path.write_text(json.dumps(jobs, indent=2) + '\n') subprocess.run(['xvfb-run', '-a', str(RENDERER), '--script', str(ROOT / 'render_batch.mjs'), '--', str(jobs_path)], check=True) # Cutaway schematic files are render intermediates; final image labels record the level. for job in jobs: path = Path(job['input']) if path.parent.parent == ROOT / 'previews': path.unlink(missing_ok=True) create_index(jobs) def create_index(jobs): """Build an offline review index with unambiguous client, seed and floor labels.""" rows = [] for job in jobs: for view in job['views']: rel = Path(job['folder']).name + '/' + view + '.png' rows.append(f'<figure><a href="{rel}"><img loading="lazy" src="{rel}"></a><figcaption>{job["name"]}: {view}</figcaption></figure>') page = '<!doctype html><meta charset="utf-8"><title>Hearthwright review</title><style>body{background:#252d32;color:#f5e9d2;font:16px system-ui;margin:2rem}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:1rem}figure{margin:0}img{width:100%}figcaption{padding:.6rem}</style><h1>Hearthwright sample review</h1><p>Exterior pairs and furnished floor cutaways. Every schematic was validated before and after export.</p><main>' + ''.join(rows) + '</main>' (ROOT / 'previews/index.html').write_text(page) if __name__ == '__main__': main() PY cp render.mjs render_batch.mjs /opt/venv/bin/python - <<'PY' p='render_batch.mjs';s=open(p).read();a=s.index('const INPUT');b=s.index('// Tuned defaults',a);s=s[:a]+"const JOBS = JSON.parse(fs.readFileSync(path.resolve(argv[0]), 'utf8'));\nconst SIZE = Number(process.env.RENDER_SIZE ?? 768);\n\n"+s[b:] a=s.index('const VIEWS =');b=s.index('// The renderer looks',a);s=s[:a]+"const VIEWS = {\n front: { azimuth: 35, elevation: 26, radius: 3.15 },\n rear: { azimuth: 210, elevation: 24, radius: 3.15 },\n floor: { azimuth: 35, elevation: 65, radius: 3.0 },\n};\n\n"+s[b:] s=s.replace('for (const view of VIEWS) {', "for (const job of JOBS) {\n const absoluteInput = job.input;\n const absoluteOutDir = job.folder;\n for (const name of job.views) {\n const view = {...VIEWS[name.startsWith('floor') ? 'floor' : name], name};") s+='\n}\n';open(p,'w').write(s) PY /opt/venv/bin/python -m scripts.sample_batch

日志保留的输出末尾

mediterranean_villa seed=0: 7594 blocks, validated after reload mediterranean_villa seed=1: 6515 blocks, validated after reload merchant_manor seed=0: 8962 blocks, validated after reload merchant_manor seed=1: 8109 blocks, validated after reload red_roof_farmstead seed=0: 6052 blocks, validated after reload red_roof_farmstead seed=1: 5183 blocks, validated after reload root_house seed=0: 6589 blocks, validated after reload root_house seed=1: 5007 blocks, validated after reload sandstone_cottage seed=0: 9133 blocks, validated after reload sandstone_cottage seed=1: 8002 blocks, validated after reload stilt_water_cabin seed=0: 4865 blocks, validated after reload stilt_water_cabin seed=1: 4071 blocks, validated after reload stilted_manor seed=0: 7296 blocks, validated after reload stilted_manor seed=1: 6273 blocks, validated after reload stone_cottage seed=0: 2775 blocks, validated after reload stone_cottage seed=1: 2290 blocks, validated after reload canal_apothecary seed=0: 7117 blocks, validated after reload canal_apothecary seed=1: 6658 blocks, validated after reload woodland_archive seed=0: 4971 blocks, validated after reload woodland_archive seed=1: 4433 blocks, validated after reload orchard_house seed=0: 4842 blocks, validated after reload orchard_house seed=1: 4756 blocks, validated after reload
#037 · 2026-09-25T15:35:16.004Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from hearthwright.blocks import NBTFile from pathlib import Path f=NBTFile.load_regardless_of_gzipped(Path('samples/stone_cottage-0.litematic')) region=next(iter(f['Regions'].values())) print(type(region));print(str(region['TileEntities'])[:900]);print(region['TileEntities'].unpack()[:2]) PY

日志保留的输出末尾

<class 'mcio.nbt.tag.Compound'> List[Compound]([Compound({'id': String('minecraft:barrel'), 'x': Int(9), 'y': Int(3), 'z': Int(17), 'Items': List[Compound]([Compound({'Slot': Byte(0), 'id': String('minecraft:bread'), 'count': Int(6)}), Compound({'Slot': Byte(1), 'id': String('minecraft:oak_log'), 'count': Int(12)})])}), Compound({'id': String('minecraft:bed'), 'x': Int(9), 'y': Int(8), 'z': Int(9)}), Compound({'id': String('minecraft:barrel'), 'x': Int(10), 'y': Int(3), 'z': Int(17), 'Items': List[Compound]([Compound({'Slot': Byte(0), 'id': String('minecraft:bread'), 'count': Int(6)}), Compound({'Slot': Byte(1), 'id': String('minecraft:oak_log'), 'count': Int(12)})])}), Compound({'id': String('minecraft:bed'), 'x': Int(10), 'y': Int(8), 'z': Int(9)}), Compound({'id': String('minecraft:smoker'), 'x': Int(17), 'y': Int(3), 'z': Int(9), 'Items': List[Compound]([Compound({'Slot': Byte(0), 'id': String('minecraft:bread'), ' [{'id': 'minecraft:barrel', 'x': 9, 'y': 3, 'z': 17, 'Items': [{'Slot': 0, 'id': 'minecraft:bread', 'count': 6}, {'Slot': 1, 'id': 'minecraft:oak_log', 'count': 12}]}, {'id': 'minecraft:bed', 'x': 9, 'y': 8, 'z': 9}]
#038 · 2026-09-25T15:37:19.933Z · exit=0 · cat > hearthwright/components.py <<'PY'
cat > hearthwright/components.py <<'PY' # Extension contracts for reusable site components with shared planning and validation. from dataclasses import dataclass from typing import Callable @dataclass(frozen=True) class SiteComponent: """Register a reusable geometry algorithm, not a complete building preset. Args: name: Unique capability identifier used by Feature. realize: Callable receiving Compiler context and the resolved feature dictionary. It must set feature['target'] to an accessible feet position, append a metadata feature with owner/kind/target, and record supports/reservations. scale: Inclusive minimum and maximum plot width/depth. rule: Optional validator receiving Grid, metadata and Report. version: Explicit implementation version retained in the resolved plan. """ name: str realize: Callable scale: tuple[int, int] = (5, 11) rule: Callable | None = None version: str = '1' SITE_COMPONENTS = {} BUILTIN_FEATURES = ('pool', 'field', 'well', 'windmill', 'landing', 'garden') def register_site_component(component: SiteComponent): """Integrate a component with plot allocation, route planning and validation.""" if component.name in SITE_COMPONENTS or component.name in BUILTIN_FEATURES: raise ValueError(f'component name already registered: {component.name}') if not 3 <= component.scale[0] <= component.scale[1] <= 15: raise ValueError('component plot scale must lie in 3..15') SITE_COMPONENTS[component.name] = component return component PY /opt/venv/bin/python - <<'PY' p='hearthwright/api.py';s=open(p).read().replace(" return candidates[rng.randrange(len(candidates))] if len(candidates) > 1 else candidates[0]", " if hasattr(rng, 'pick'):\n return rng.pick(candidates)\n return candidates[rng.randrange(len(candidates))] if len(candidates) > 1 else candidates[0]") open(p,'w').write(s) p='hearthwright/plan.py';s=open(p).read().replace('from .materials import WALLS', 'from .components import SITE_COMPONENTS, BUILTIN_FEATURES\nfrom .materials import WALLS') s=s.replace("def resolve(design: Design, seed: int) -> Plan:", "def _resolve(design: Design, seed: int, rng) -> Plan:").replace(' rng = random.Random(seed)\n mat =', ' mat =') s=s.replace("if f.kind not in ('pool', 'field', 'well', 'windmill', 'landing', 'garden'):", "if f.kind not in (*BUILTIN_FEATURES, *SITE_COMPONENTS):") s=s.replace(" size = choose(f.scale, rng, lambda n: type(n) is int and 5 <= n <= 11, 'feature.scale')", " scale_domain = SITE_COMPONENTS[f.kind].scale if f.kind in SITE_COMPONENTS else (5, 11)\n size = choose(f.scale, rng, lambda n: type(n) is int and scale_domain[0] <= n <= scale_domain[1], 'feature.scale')") s=s.replace("kind=f.kind, side=side_f, rect=fr)", "kind=f.kind, side=side_f, rect=fr, version=SITE_COMPONENTS[f.kind].version if f.kind in SITE_COMPONENTS else '1')") s += '''\n\nclass _DecisionRandom(random.Random): """Retain finite decisions so later constraints can backtrack within declared domains.""" def __init__(self, seed, tape): super().__init__(seed) self.tape = tape self.visited = [] def pick(self, candidates): if len(candidates) == 1: return candidates[0] first = self.randrange(len(candidates)) index = self.tape[len(self.visited)] if len(self.visited) < len(self.tape) else 0 index = min(index, len(candidates) - 1) self.visited.append((index, len(candidates))) return candidates[(first + index) % len(candidates)] def resolve(design: Design, seed: int, *, max_candidates=128) -> Plan: """Search bounded declared choices, retaining explicit decisions without fallback presets. Args: design: Architectural composition, with concrete values or bounded domains. seed: Signed 64-bit integer controlling all choice ordering. max_candidates: Maximum complete or partial plans examined before a diagnostic. Returns: Plan: Feasible resolved architecture with no geometric mutations to user intent. """ if type(seed) is not int or not -(2**63) <= seed < 2**63: raise DesignError('seed must be a signed 64-bit integer') tape = [] error = None for attempt in range(max_candidates): rng = _DecisionRandom(seed, tape) try: plan = _resolve(design, seed, rng) return plan except DesignError as exc: error = exc visited = rng.visited for i in range(len(visited) - 1, -1, -1): branch, count = visited[i] if branch + 1 < count: tape = [v[0] for v in visited[:i]] + [branch + 1] break else: raise DesignError(f'contradictory specification: {exc}') from exc raise DesignError(f'planning budget ({max_candidates} candidates) exhausted: {error}; narrow the conflicting domains') ''' open(p,'w').write(s) p='hearthwright/site.py';s=open(p).read().replace('from .blocks import is_air', 'from .components import SITE_COMPONENTS\nfrom .blocks import is_air').replace(" r, kind = f['rect'], f['kind']\n cx, cz", " r, kind = f['rect'], f['kind']\n if kind in SITE_COMPONENTS:\n SITE_COMPONENTS[kind].realize(ctx, f)\n return\n cx, cz");open(p,'w').write(s) p='hearthwright/validate.py';s=open(p).read().replace('from .blocks import Grid', 'from .components import SITE_COMPONENTS\nfrom .blocks import Grid').replace(" report.checked['blocks'] = len(g.blocks)", " for kind in {f['kind'] for f in m.get('features', [])}:\n if kind in SITE_COMPONENTS and SITE_COMPONENTS[kind].rule:\n SITE_COMPONENTS[kind].rule(g, m, report)\n report.checked['blocks'] = len(g.blocks)") # Verify actual NBT on reload instead of trusting the JSON inventory declaration. s=s.replace(" report.checked['furniture_groups'] = len(m['furniture'])", " for record in m.get('inventories', []):\n p = tuple(record['point'])\n expected = [tuple(item) for item in record['items']]\n actual = [tuple(item) for item in g.inventories.get(p, [])]\n if expected != actual:\n report.add('inventory.changed', 'container', 'actual block entity inventory differs from the declared supplies', p)\n report.checked['furniture_groups'] = len(m['furniture'])") open(p,'w').write(s) p='hearthwright/blocks.py';s=open(p).read().replace(" grid.inventories = {tuple(record['point']): record['items'] for record in metadata.get('inventories', [])}", " nbt = NBTFile.load_regardless_of_gzipped(path)\n region = next(iter(nbt['Regions'].values()))\n for tag in region.get('TileEntities', []).unpack():\n if 'Items' in tag:\n point = tag['x'] + ox, tag['y'] + oy, tag['z'] + oz\n grid.inventories[point] = [(item['id'].removeprefix('minecraft:'), item['count']) for item in sorted(tag['Items'], key=lambda item: item['Slot'])]") open(p,'w').write(s) p='hearthwright/__init__.py';s=open(p).read();s += '\nfrom .components import SiteComponent, register_site_component\n';s=s.replace("'blockstates_equivalent']", "'blockstates_equivalent', 'SiteComponent', 'register_site_component']");open(p,'w').write(s) PY /opt/venv/bin/python -m scripts.render_batch > previews/render.log 2>&1

日志保留的输出末尾

#039 · 2026-09-25T15:40:06.251Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' try: from PIL import Image,ImageDraw print('Pillow available') names=['mediterranean_villa','merchant_manor','red_roof_farmstead','root_house','sandstone_cottage','stilt_water_cabin','stilted_manor','stone_cottage','canal_apothecary','woodland_archive','orchard_house'] canvas=Image.new('RGB',(1200,4*340),(37,45,50));d=ImageDraw.Draw(canvas) for i,n in enumerate(names): p=f'previews/{n}-0/front.png' im=Image.open(p).convert('RGB');im.thumbnail((390,310));x=(i%3)*400;y=(i//3)*340;canvas.paste(im,(x,y));d.text((x+12,y+316),n,fill=(255,240,220)) canvas.save('previews/exterior-overview.jpg') except Exception as e:print(type(e).__name__,e) PY

日志保留的输出末尾

ModuleNotFoundError No module named 'PIL'
#040 · 2026-09-25T15:40:19.575Z · exit=0 · cat > test/conftest.py <<'PY'
cat > test/conftest.py <<'PY' # Keep all transient test artifacts under test/ and remove them after each test. from pathlib import Path import tempfile import pytest @pytest.fixture def artifact_dir(): root = Path(__file__).parent / '.tmp' root.mkdir(exist_ok=True) with tempfile.TemporaryDirectory(dir=root) as directory: yield Path(directory) try: root.rmdir() except OSError: pass PY cat > test/test_api.py <<'PY' # Exercise public composition, dependent domains and explicit intent preservation. from dataclasses import replace import pytest from hearthwright import Choice, Span, Design, DesignError, Level, Volume, Roof, Join, Site, Feature, Materials, Limits, generate, resolve, SiteComponent, register_site_component def test_composition_needs_no_reference_dispatch(): design = Design('Independent client', ( Volume('home', width=Span(13, 17, 2), depth=15, levels=(Level(('living', 'kitchen')), Level(('sleeping',), attic=True)), roof=Roof(ridge=Choice('x', 'z'), pitch=1.0)), Volume('work', width=9, depth=11, join=Join('home', 'east', Choice('front', 'back')), levels=(Level(('workshop', 'storage')),), roof=Roof(form='hip', pitch=.5)), ), materials=Materials(wall='stone', roof='red')) building = generate(design, 42) assert building.validate().valid assert building.plan.masses[1].join['to'] == 'home' assert building.plan.masses[0].rect.width in (13, 15, 17) assert building.plan.masses[1].roof['form'] == 'hip' def test_bounded_floor_count_changes_actual_geometry(): from examples.orchard_house import DESIGN buildings = [generate(DESIGN, seed) for seed in range(5)] assert {len(b.plan.masses[0].levels) for b in buildings} == {1, 2} assert len({b.metadata['geometry_signature'] for b in buildings}) > 1 def test_conditional_choice_does_not_discard_explicit_roof(): design = Design('Conditional loft', (Volume('home', width=15, depth=13, levels=(Level(('living', 'kitchen')), Level(('sleeping',), attic=True)), roof=Roof(pitch=Choice(.5, 1.0, 1.5), dormers=1)),)) for seed in range(6): plan = resolve(design, seed) assert plan.masses[0].roof['pitch'] in (1.0, 1.5) assert plan.masses[0].roof['dormers'] == 1 def test_planner_backtracks_occupied_wing_domains(): design = Design('Two wings', ( Volume('main', width=13, depth=13), Volume('first', join=Join('main', 'east'), width=9, depth=9), Volume('second', join=Join('main', Choice('east', 'west')), width=9, depth=9), )) for seed in range(4): building = generate(design, seed) assert building.plan.masses[2].join['side'] == 'west' @pytest.mark.parametrize('design,match', [ (Design('bad water', (Volume('house'),), site=Site(terrain='water')), 'water buildings'), (Design('low loft', (Volume('house', levels=(Level(('sleeping',), attic=True),), roof=Roof(pitch=.5)),)), 'roof.pitch'), (Design('root roof', (Volume('root', envelope='roots'),)), 'roof=None'), (Design('tiny', (Volume('home', width=7),)), 'width'), (Design('duplicate', (Volume('home'), Volume('home'))), 'duplicate'), (Design('oversized', (Volume('home'),), limits=Limits(horizontal=15)), 'resource limit'), (Design('unsupported material', (Volume('home'),), materials=Materials(roof='wool')), 'roof role'), (Design('missing join', (Volume('a'), Volume('b'))), 'Join'), ]) def test_reject_contradictory_intent(design, match): with pytest.raises(DesignError, match=match): generate(design, 0) @pytest.mark.parametrize('seed', [-(2**63) - 1, 2**63, 1.5, '1', True]) def test_seed_domain(seed): with pytest.raises(DesignError, match='signed 64-bit'): resolve(Design('seed', (Volume('home'),)), seed) def test_invalid_domains(): with pytest.raises(ValueError): Span(10, 3) with pytest.raises(ValueError): Span(3, 10, 0) with pytest.raises(ValueError): Choice() def test_registered_component_uses_shared_path_and_validation(): from hearthwright.components import SITE_COMPONENTS calls = [] def realize(ctx, feature): r = feature['rect'] x, z = r.center y = ctx.ground[x, z] ctx.g.put(x, y + 1, z, 'barrel') feature['target'] = (x, y + 1, r.z1 + 1) ctx.g.metadata['features'].append({'owner': feature['id'], 'kind': feature['kind'], 'target': feature['target']}) def rule(grid, metadata, report): calls.append(len(metadata['features'])) component = SiteComponent('tool_cache', realize, rule=rule, version='test-1') try: register_site_component(component) building = generate(Design('Extension', (Volume('home'),), features=(Feature('tool_cache'),)), 0) assert building.validate().valid assert calls assert building.plan.features[0]['version'] == 'test-1' finally: SITE_COMPONENTS.pop('tool_cache', None) PY cat > test/test_corpus.py <<'PY' # Validate a diverse finite seed corpus through independent high-level clients. import importlib from dataclasses import replace import pytest from hearthwright import Design, Volume, Level, Roof, Site, Materials, generate from scripts.sample_batch import CLIENTS SEEDS = (0, 1, 2, 3, 4, 5, 17, 101, 20260925, -1, 2**63 - 1, -(2**63)) @pytest.mark.parametrize('client', CLIENTS) @pytest.mark.parametrize('seed', SEEDS) def test_reference_and_novel_compositions(client, seed): design = importlib.import_module('examples.' + client).DESIGN building = generate(design, seed) report = building.validate() assert report.valid, report.to_dict() assert report.checked['rooms'] >= 1 assert report.checked['furniture_groups'] >= len(building.metadata['rooms']) assert all(room['functions'] for room in building.metadata['rooms']) @pytest.mark.parametrize('width,depth,height,pitch,ridge', [(11, 13, 7, 1.5, 'z'), (25, 23, 5, 1.0, 'x'), (15, 15, 6, 1.5, 'z')]) def test_dimension_and_pitch_boundaries(width, depth, height, pitch, ridge): design = Design('Boundary specimen', (Volume('home', width=width, depth=depth, levels=(Level(('living', 'kitchen'), height=height), Level(('sleeping', 'study'), attic=True)), roof=Roof(pitch=pitch, ridge=ridge, dormers=1)),)) assert generate(design, 7).validate().valid def test_diversity_is_structural_and_realized(): signatures = set() geometries = set() for client in CLIENTS: design = importlib.import_module('examples.' + client).DESIGN a, b = (generate(design, seed) for seed in (0, 1)) assert a.metadata['structural_signature'] != b.metadata['structural_signature'], client assert a.metadata['geometry_signature'] != b.metadata['geometry_signature'], client assert a.grid.blocks != b.grid.blocks signatures.update([a.metadata['structural_signature'], b.metadata['structural_signature']]) geometries.update([a.metadata['geometry_signature'], b.metadata['geometry_signature']]) assert len(signatures) == len(CLIENTS) * 2 assert len(geometries) == len(CLIENTS) * 2 def test_water_posts_are_not_manor_overhangs(): from examples.stilt_water_cabin import DESIGN as WATER from examples.stilted_manor import DESIGN as MANOR wet = generate(WATER, 0) dry = generate(MANOR, 0) assert wet.plan.masses[0].supports == 'posts' assert wet.plan.masses[0].levels[0].floor > WATER.site.water_depth assert dry.plan.masses[0].levels[0].rect.width < dry.plan.masses[0].levels[1].rect.width assert dry.plan.design.site.terrain != 'water' assert any(s['owner'] == 'manor/1' for s in dry.metadata['supports']) def test_root_envelope_is_nonrectangular_and_buried_floor_excavated(): from examples.root_house import DESIGN as ROOT from examples.sandstone_cottage import DESIGN as SLOPE root = generate(ROOT, 0) level = root.plan.masses[0].levels[0] assert len(level.cells) < level.rect.width * level.rect.depth assert any('dark_oak_log' in s and (x, z) not in level.cells for (x, y, z), s in root.grid.blocks.items()) slope = generate(SLOPE, 0) assert slope.plan.design.site.terrain == 'slope' assert any('water' in state for state in slope.grid.blocks.values()) assert slope.validate().valid PY cat > test/test_roundtrip.py <<'PY' # Check deterministic transport, semantic state equality and reloaded validation. import importlib from pathlib import Path import pytest from hearthwright import Building, Design, Volume, generate, validate, blockstates_equivalent from hearthwright.blocks import NBTFile from scripts.sample_batch import CLIENTS def test_semantic_property_order_is_not_a_change(): a = 'minecraft:oak_stairs[facing=north,half=bottom,shape=straight]' b = 'minecraft:oak_stairs[shape=straight,half=bottom,facing=north]' assert blockstates_equivalent(a, b) assert not blockstates_equivalent(a, b.replace('north', 'south')) assert not blockstates_equivalent(a, a.replace(',shape=straight', '')) assert not blockstates_equivalent(a, a.replace('oak_stairs', 'spruce_stairs')) with pytest.raises((TypeError, ValueError)): blockstates_equivalent(a, None) def test_export_is_byte_deterministic_with_overwrites(artifact_dir): design = Design('Determinism', (Volume('home'),)) a, b = generate(design, 73), generate(design, 73) p = next(iter(a.grid.blocks)) original = a.grid.get(*p) a.grid.put(*p, 'gold_block') a.grid.put(*p, original) first, second = artifact_dir / 'a.litematic', artifact_dir / 'b.litematic' a.export(first) b.export(second) assert first.read_bytes() == second.read_bytes() assert a.grid.blocks == b.grid.blocks a.verify(first) @pytest.mark.parametrize('client', CLIENTS) def test_reloaded_samples_validate_with_explicit_metadata(client, artifact_dir): design = importlib.import_module('examples.' + client).DESIGN building = generate(design, 1) path = artifact_dir / f'{client}.litematic' building.export(path) building.verify(path) assert validate(path, path.with_suffix('.json')).valid loaded = Building.load(path, building.metadata) assert loaded.validate().valid assert loaded.grid.inventories def test_reload_detects_unexpected_or_changed_geometry(artifact_dir): design = Design('Roundtrip damage', (Volume('home'),)) b = generate(design, 0) path = artifact_dir / 'build.litematic' b.export(path) loaded = Building.load(path) p = b.metadata['rooms'][0]['anchor'] loaded.grid.put(*p, 'stone') loaded.grid.export(path) with pytest.raises(AssertionError, match='coordinate set|changed on reload'): b.verify(path) def test_inventory_corruption_is_observed_in_nbt(artifact_dir): b = generate(Design('Stocked home', (Volume('home'),)), 0) path = artifact_dir / 'stock.litematic' b.export(path) nbt = NBTFile.load_regardless_of_gzipped(path) region = next(iter(nbt['Regions'].values())) for tile in region['TileEntities']: if 'Items' in tile and len(tile['Items']): del tile['Items'][:] break NBTFile(nbt).save(path) assert 'inventory.changed' in {d.rule for d in validate(path).diagnostics} PY cat > test/test_validation.py <<'PY' # Deliberately damage independent aspects of final geometry to audit rule sensitivity. import pytest from hearthwright import Design, Volume, generate from hearthwright.blocks import name def rules(building): return {d.rule for d in building.validate().diagnostics} def test_bad_door_half_is_rejected(): b = generate(Design('Door test', (Volume('home'),)), 0) p = next(p for p, s in b.grid.blocks.items() if '_door[' in s and 'half=upper' in s) b.grid.put(*p, 'spruce_door', half='upper', facing='east') assert 'multiblock.door' in rules(b) def test_missing_bed_half_and_support_are_rejected(): b = generate(Design('Bed test', (Volume('home'),)), 0) p = next(p for p, s in b.grid.blocks.items() if '_bed[' in s and 'part=head' in s) b.grid.put(*p, 'air') assert 'multiblock.bed' in rules(b) foot = next(p for p, s in b.grid.blocks.items() if '_bed[' in s) b.grid.put(foot[0], foot[1] - 1, foot[2], 'air') assert 'support.bed' in rules(b) def test_blocked_stair_headroom_and_unreachable_level_are_rejected(): from examples.stilted_manor import DESIGN b = generate(DESIGN, 0) stairs = [s for s in b.metadata['stairs'] if 'manor/0->' in s['owner']] for s in stairs: x, y, z = s['point'] b.grid.put(x, y + 1, z, 'stone') b.grid.put(x, y + 2, z, 'stone') result = rules(b) assert 'clearance.route' in result assert 'reachability.room' in result def test_stair_facing_and_tread_support_are_rejected(): from examples.red_roof_farmstead import DESIGN b = generate(DESIGN, 0) s = b.metadata['stairs'][2] x, y, z = s['point'] b.grid.stair(x, y, z, facing='north') b.grid.put(x, y - 1, z, 'air') assert {'circulation.stair', 'support.stair'} <= rules(b) def test_missing_stilt_segment_is_rejected(): from examples.stilt_water_cabin import DESIGN b = generate(DESIGN, 0) support = b.metadata['supports'][0] x, y, z = support['bottom'] b.grid.put(x, y + 1, z, 'water', level='0') assert 'support.column' in rules(b) def test_ladder_attachment_is_checked(): from examples.stone_cottage import DESIGN b = generate(DESIGN, 0) p = next(p for p, state in b.grid.blocks.items() if name(state) == 'ladder') b.grid.put(p[0] - 1, p[1], p[2], 'air') assert 'support.ladder' in rules(b) def test_wall_hole_and_blocked_glazing_are_rejected(): b = generate(Design('Envelope test', (Volume('home'),)), 0) window = next(w for w in b.metadata['openings'] if w['kind'] == 'window') p = window['cells'][0] b.grid.put(*p, 'air') assert {'opening.glazing', 'envelope.leak'} <= rules(b) def test_roof_hole_is_rejected(): from examples.stone_cottage import DESIGN b = generate(DESIGN, 0) room = b.metadata['rooms'][-1] x, y, z = room['anchor'] roof_y = b.grid.bounds[4] for yy in range(y + 2, roof_y + 1): b.grid.put(x, yy, z, 'air') assert 'envelope.leak' in rules(b) def test_remove_room_light_and_change_furniture(): b = generate(Design('Lighting test', (Volume('home'),)), 0) lamp = next(record for record in b.metadata['lights'] if '/room' in record['owner']) b.grid.put(*lamp['point'], 'air') assert {'lighting.room', 'furnishing.changed'} <= rules(b) def test_unknown_block_state_and_out_of_recorded_bounds(artifact_dir): b = generate(Design('Bounds test', (Volume('home'),)), 0) b.export(artifact_dir / 'bounds.litematic') b.grid.blocks[1000, 1000, 1000] = 'minecraft:oak_log[axis=invalid]' result = rules(b) assert {'block.state', 'bounds.export', 'bounds.resources'} <= result PY /opt/venv/bin/python -m pytest -q

日志保留的输出末尾

........................................................................ [ 39%] ........................................................................ [ 79%] ...................................... [100%] 182 passed in 25.55s
#041 · 2026-09-25T15:44:10.659Z · exit=1 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/furnish.py';s=open(p).read().replace('def modules(function, mat):', 'def modules(function, mat, area):') s=s.replace(" return programs[function]", """ if area >= 75: if function == 'living': programs[function] = [ ('settle', [(x, 0, 0, mat.floor + '_stairs', {'facing': 'north'}) for x in range(3)] + [(3, 0, 0, mat.floor + '_planks', {}), (3, 1, 0, 'potted_fern', {})]), ('dining', [(0, 0, 0, mat.floor + '_fence', {}), (2, 0, 0, mat.floor + '_fence', {})] + [(x, 1, 0, mat.floor + '_slab', {'type': 'top'}) for x in range(3)] + [(0, 2, 0, 'flower_pot', {})] + [(x, 0, z, mat.floor + '_stairs', {'facing': 'north' if z < 0 else 'south'}) for x in (0, 2) for z in (-1, 1)]), ] if function == 'kitchen': programs[function] = [('kitchen_counter', programs[function][0][1] + [(3, 0, 0, 'barrel', {'facing': 'south'}), (3, 1, 0, 'potted_red_tulip', {})]), ('cupboard', [(0, 0, 0, 'barrel', {'facing': 'south'}), (0, 1, 0, 'barrel', {'facing': 'south'})])] if function == 'sleeping': programs[function] = [('double_bed', [(x, 0, z, color + '_bed', {'part': part, 'facing': 'north'}) for x in (0, 1) for z, part in ((0, 'foot'), (-1, 'head'))]), ('wardrobe', [(0, 0, 0, 'barrel', {'facing': 'south'}), (0, 1, 0, 'barrel', {'facing': 'south'}), (1, 0, 0, 'bookshelf', {})])] if function == 'storage': programs[function] = [('stores', [(x, y, 0, 'barrel', {'facing': 'south'}) for x in range(3) for y in (0, 1)])] if function == 'study': programs[function] = [('library', [(x, y, 0, 'bookshelf', {}) for x in range(3) for y in (0, 1)]), ('writing_desk', [(0, 0, 0, 'cartography_table', {}), (1, 0, 0, mat.floor + '_planks', {}), (1, 1, 0, 'potted_fern', {}), (0, 0, 1, mat.floor + '_stairs', {'facing': 'south'})])] if function == 'enchanting': programs[function] = [('enchantment', [(0, 0, 0, 'enchanting_table', {})]), ('books', [(x, y, 0, 'bookshelf', {}) for x in range(3) for y in (0, 1)])] return programs[function]""") s=s.replace(" candidates.sort(key=lambda p: sum((p[0] + dx, p[1] + dz) in cells for dx, dz in DIRECTIONS.values()))", " if kind == 'dining':\n candidates.sort(key=lambda p: abs(p[0] - room['anchor'][0]) + abs(p[1] - room['anchor'][2]))\n else:\n candidates.sort(key=lambda p: sum((p[0] + dx, p[1] + dz) in cells for dx, dz in DIRECTIONS.values()))") s=s.replace('modules(function, mat)', "modules(function, mat, len(room['cells']))") s=s.replace(" # Inlaid rugs add domestic color", """ if len(room['cells']) > 100 and room['height'] >= 5: # Two supported pendant lamps illuminate larger domestic rooms. ax, _, az = room['anchor'] candidates = sorted(room['cells'], key=lambda p: abs(p[0] - ax) + abs(p[1] - az)) pendants = [] for x, z in candidates: y = room['floor'] + room['height'] - 1 if any(abs(x - px) + abs(z - pz) < 6 for px, pz in pendants): continue if full_support(g.get(x, y + 1, z)) and g.free([(x, y, z)]): g.lantern(x, y, z, room['id'], True) pendants.append((x, z)) if len(pendants) == 2: break # Inlaid rugs add domestic color""") s=s.replace('for x in range(ax - 1, ax + 2):', 'for x in range(ax - 2, ax + 3):').replace("g.put(x, room['floor'], z, mat.accent + '_terracotta')", "g.put(x, room['floor'], z, (mat.accent if abs(x - ax) == 2 or abs(z - az) == 1 else 'white') + '_terracotta')") open(p,'w').write(s) p='hearthwright/build.py';s=open(p).read();needle=" if not s.attic:\n g.put(x, s.floor + s.height, z, mat.floor + '_planks')"; repl=""" if not s.attic: ceiling = s.floor + s.height if mass.envelope == 'roots' and i == len(mass.levels) - 1: depth = min(x - s.rect.x0, s.rect.x1 - x, z - s.rect.z0, s.rect.z1 - z) # A dark organic cap buries the ceiling into the trunk shoulder. for y in range(ceiling, ceiling + 2 + depth // 2): if is_air(g.get(x, y, z)) or y == ceiling: g.put(x, y, z, 'dark_oak_log', axis='y') if depth < 2 and is_air(g.get(x, ceiling + 2, z)): g.put(x, ceiling + 2, z, 'moss_block') else: g.put(x, ceiling, z, mat.floor + '_planks')""" s=s.replace(needle,repl);open(p,'w').write(s) p='examples/root_house.py';s=open(p).read().replace("Level(('sleeping', 'study'), wall='earth')", "Level(('sleeping', 'study'), wall='earth', inset=1)");open(p,'w').write(s) p='hearthwright/api.py';s=open(p).read().replace(" water_depth: int = 2", " water_depth: int = 2\n paving: str = 'garden'");open(p,'w').write(s) p='hearthwright/plan.py';s=open(p).read().replace(" if design.site.planting not in", " if design.site.paving not in ('garden', 'courtyard', 'yard'):\n raise DesignError('site paving must be garden, courtyard or yard')\n if design.site.planting not in");open(p,'w').write(s) p='hearthwright/site.py';s=open(p).read();s=s.replace(" g.put(x, h, z, 'grass_block')\n ctx.ground", " finish = 'grass_block'\n if site.paving != 'garden' and p.site.inset(4).contains(x, z):\n finish = ('terracotta' if x % 5 < 2 and z % 5 < 2 else 'smooth_sandstone') if site.paving == 'courtyard' else g.rng.choice(('coarse_dirt', 'coarse_dirt', 'gravel'))\n g.put(x, h, z, finish)\n ctx.ground") s=s.replace("block = 'smooth_sandstone' if a['kind'] == 'terrace' else mat.floor + '_planks'", "block = ('smooth_sandstone' if mat.wall == 'white' else 'stone_bricks') if a['kind'] == 'terrace' else mat.floor + '_planks'") s=s.replace("'terracotta' if a['kind'] == 'terrace' else log(mat.floor)", "('terracotta' if mat.wall == 'white' else 'andesite') if a['kind'] == 'terrace' else log(mat.floor)") open(p,'w').write(s) p='examples/mediterranean_villa.py';s=open(p).read().replace("Site(planting='mediterranean')", "Site(planting='mediterranean', paving='courtyard', margin=3)");open(p,'w').write(s) p='examples/stone_cottage.py';s=open(p).read().replace('Roof, Volume', 'Roof, Site, Volume').replace(" materials=Materials", " site=Site(margin=3, planting='sparse'),\n materials=Materials");open(p,'w').write(s) p='hearthwright/blocks.py';s=open(p).read().replace(" for tag in region.get('TileEntities', []).unpack():", " tiles = region.get('TileEntities')\n for tag in tiles.unpack() if tiles is not None else []:");open(p,'w').write(s) PY /opt/venv/bin/python -m pytest -q

日志保留的输出末尾

ng.changed', component='house/0/room0', message='dining was removed or ov..._columns': 8, 'reachable_positions': 1509, 'rooms': 4, 'envelope_voids': 1261, 'furniture_groups': 15, 'blocks': 5792}) def require_valid(self): if not self.valid: details = '\n'.join(f'{d.rule}: {d.component} {d.location or ""}: {d.message}' for d in self.diagnostics[:25]) > raise ValidationError(f'{len(self.diagnostics)} architectural validation failures\n{details}', self) E hearthwright.validate.ValidationError: 1 architectural validation failures E furnishing.changed: house/0/room0 (4, 3, 6): dining was removed or overwritten hearthwright/validate.py:39: ValidationError =========================== short test summary info ============================ FAILED test/test_corpus.py::test_reference_and_novel_compositions[1-mediterranean_villa] FAILED test/test_corpus.py::test_reference_and_novel_compositions[3-sandstone_cottage] FAILED test/test_corpus.py::test_reference_and_novel_compositions[5-root_house] FAILED test/test_corpus.py::test_reference_and_novel_compositions[17-sandstone_cottage] FAILED test/test_corpus.py::test_reference_and_novel_compositions[101-sandstone_cottage] FAILED test/test_corpus.py::test_reference_and_novel_compositions[20260925-root_house] FAILED test/test_corpus.py::test_reference_and_novel_compositions[20260925-orchard_house] FAILED test/test_corpus.py::test_reference_and_novel_compositions[-1-mediterranean_villa] FAILED test/test_corpus.py::test_reference_and_novel_compositions[-9223372036854775808-stilt_water_cabin] FAILED test/test_corpus.py::test_reference_and_novel_compositions[-9223372036854775808-orchard_house] FAILED test/test_corpus.py::test_dimension_and_pitch_boundaries[11-13-7-1.5-z] FAILED test/test_corpus.py::test_diversity_is_structural_and_realized - heart... FAILED test/test_roundtrip.py::test_reloaded_samples_validate_with_explicit_metadata[mediterranean_villa] 13 failed, 169 passed in 22.83s
#042 · 2026-09-25T15:44:38.562Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='generate.py';s=open('examples/stilted_manor.py').read();s=s.replace('# Recessed masonry base carrying an overhanging timber house and steep inhabited roof.', '# Harness client: declare, generate, validate and export one furnished property.');s=s.replace('from hearthwright.cli import run', 'from hearthwright import blockstates_equivalent\nfrom hearthwright.cli import run');open(p,'w').write(s) PY /opt/venv/bin/python -m compileall -q hearthwright examples scripts generate.py /opt/venv/bin/yapf -i hearthwright/*.py examples/*.py scripts/*.py test/*.py generate.py rg --files hearthwright examples scripts test | wc -l

日志保留的输出末尾

66
#043 · 2026-09-25T15:45:21.817Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/build.py';s=open(p).read();needle=" g.put(x, y, z, name(state), **properties)\n\n def geometry_signature" s=s.replace(needle," g.put(x, y, z, name(state), **properties)\n # Connection properties are finalized after all neighboring furniture exists.\n for group in g.metadata['furniture']:\n for i, expected in enumerate(group['states']):\n if name(expected).endswith('_fence'):\n group['states'][i] = g.get(*group['cells'][i])\n\n def geometry_signature") open(p,'w').write(s) PY /opt/venv/bin/python -m pytest -q

日志保留的输出末尾

........................................................................ [ 39%] ........................................................................ [ 79%] ...................................... [100%] 182 passed in 25.09s
#044 · 2026-09-25T15:46:19.660Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from hearthwright import * d=Design('Joined hollow', (Volume('hollow',width=13,depth=13,envelope='roots',roof=None,levels=(Level(('living','kitchen','sleeping')),)),Volume('studio',width=9,depth=9,join=Join('hollow','east'),levels=(Level(('workshop','study')),),roof=Roof(pitch=.5))),materials=Materials(wall='earth',frame='dark_oak',roof='dark_wood'),site=Site(terrain='hill',planting='sparse')) for seed in (0,1,2): try: b=generate(d,seed);print(seed,'valid') except Exception as e:print(seed,str(e)[:1800]) PY

日志保留的输出末尾

0 valid 1 valid 2 valid
#045 · 2026-09-25T15:48:17.767Z · exit=0 · cat > examples/hillside_studio.py <<'PY'
cat > examples/hillside_studio.py <<'PY' # A new composition: hollow roots joined to a glazed masonry workshop wing. from hearthwright import Choice, Design, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Elderroot studio', volumes=( Volume('hollow', width=Choice(13, 15), depth=Choice(11, 13), envelope='roots', roof=None, root_crown=Choice(3, 5), levels=(Level(('living', 'kitchen', 'sleeping')),)), Volume('studio', width=9, depth=Choice(9, 11), join=Join('hollow', Choice('east', 'west'), 'center'), levels=(Level(('workshop', 'study')),), roof=Roof(pitch=.5)), ), materials=Materials(wall='earth', frame='dark_oak', roof='dark_wood', accent='green'), site=Site(terrain='hill', planting='sparse'), ) if __name__ == '__main__': run(DESIGN) PY /opt/venv/bin/python - <<'PY' p='scripts/sample_batch.py';s=open(p).read().replace("'orchard_house',", "'orchard_house', 'hillside_studio',");open(p,'w').write(s) p='hearthwright/plan.py';s=open(p).read();needle=" cursors[side_f] += size + 3\n features.append";s=s.replace(needle," for previous in features:\n if fr.inset(-1).cells() & previous['rect'].cells():\n raise DesignError(f'feature {index} overlaps {previous[\"id\"]}; change its side or scale')\n cursors[side_f] += size + 3\n features.append");open(p,'w').write(s) PY cat > scripts/audit.py <<'PY' # Repeatable bounded seed audit with observed structural diversity and full diagnostics. import argparse import importlib import json from pathlib import Path from hearthwright import generate from scripts.sample_batch import CLIENTS def main(): """Audit a finite consecutive seed corpus without producing temporary schematics.""" parser = argparse.ArgumentParser() parser.add_argument('--count', type=int, default=64) parser.add_argument('--start', type=int, default=0) parser.add_argument('--report', type=Path, default=Path('samples/audit.json')) args = parser.parse_args() if not 1 <= args.count <= 256: parser.error('count must be 1..256 for a bounded audit') result = {'seeds': list(range(args.start, args.start + args.count)), 'clients': {}, 'failures': []} for client in CLIENTS: design = importlib.import_module('examples.' + client).DESIGN structures, geometries = set(), set() for seed in result['seeds']: try: building = generate(design, seed) structures.add(building.metadata['structural_signature']) geometries.add(building.metadata['geometry_signature']) except Exception as exc: result['failures'].append({'client': client, 'seed': seed, 'error': str(exc)}) print(f'FAILED {client} seed={seed}: {exc}', flush=True) result['clients'][client] = {'observed_structures': len(structures), 'observed_geometry_signatures': len(geometries)} print(f'{client}: audited {args.count} seeds', flush=True) args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, indent=2, sort_keys=True) + '\n') if result['failures']: raise SystemExit(1) if __name__ == '__main__': main() PY /opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1

日志保留的输出末尾

#046 · 2026-09-25T15:50:47.862Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/plan.py';s=open(p).read().replace(" if design.site.planting not in", " if design.site.terrain == 'water' and design.site.paving != 'garden':\n raise DesignError('water sites require garden paving; use supported deck attachments for hard surfaces')\n if design.site.planting not in");open(p,'w').write(s) p='hearthwright/build.py';s=open(p).read();needle=" for x, z in sorted(points):\n if (x, z) in below.cells:";repl=" points.update((x, z) for x in (r.x0, r.x1) for z in (r.z0 + r.depth // 3, r.z1 - r.depth // 3))\n for x, z in sorted(points):\n if (x, z) in below.cells:";s=s.replace(needle,repl) needle=" g.stair(x + dx, s.floor - 1, z + dz, mat.floor, next(k for k, delta in DIRECTIONS.items() if delta == (-dx, -dz)), 'top')";repl=" g.stair(x + dx, s.floor - 1, z + dz, mat.floor, next(k for k, delta in DIRECTIONS.items() if delta == (-dx, -dz)), 'top')\n g.stair(x + dx, s.floor - 2, z + dz, mat.floor, next(k for k, delta in DIRECTIONS.items() if delta == (-dx, -dz)), 'top')";s=s.replace(needle,repl) open(p,'w').write(s) PY cat > GENERATOR.md <<'MD' # Hearthwright Hearthwright is a Python library for composing furnished Minecraft Java 1.21.1 properties. Clients describe connected volumes, occupied levels, room programs, roofs, terrain relationships and outdoor spaces. The shared library resolves and constructs them, protects circulation, furnishes rooms, validates the resulting blocks and exports one deterministic Litematic region. There is no building-family argument, reference dispatch or embedded collection of complete buildings. The examples contain architectural declarations using the same public classes. The four new compositions use those classes without custom geometry or validator code. ## Run the delivered artifacts From `/work/generator/workspace`: ```sh /opt/venv/bin/python generate.py /opt/venv/bin/python generate.py --seed 73 --output samples/my-manor.litematic /opt/venv/bin/python -m examples.stone_cottage --seed 8 --output samples/cottage.litematic /opt/venv/bin/python -m examples.hillside_studio --seed 31 --output samples/studio.litematic # All twelve independent clients, seeds 0 and 1, with reload validation and manifest. /opt/venv/bin/python -m scripts.sample_batch # All clients: two exterior views and each main-volume occupied floor as a cutaway. /opt/venv/bin/python -m scripts.render_batch # Include every saved seed in the render review: /opt/venv/bin/python -m scripts.render_batch --all-seeds /opt/venv/bin/python -m pytest -q /opt/venv/bin/python -m scripts.audit --count 64 ``` `generate.py` is a standalone architectural client. Its default seed is **20260925**, and its default destination is **output.litematic**. Every example accepts the same `--seed` and `--output` options. Example modules run with `python -m examples.NAME`; the name selects a Python client file, not a library building preset. The environment supplies MCIO, NumPy and the Java state registry. No downloads or reference-script execution occur. To use the package from another directory: ```sh export PYTHONPATH=/work/generator/workspace cd /tmp /opt/venv/bin/python /work/generator/workspace/generate.py --seed 73 --output /tmp/property.litematic ``` Alternatively, install the local Python package with `uv pip install --python /opt/venv/bin/python --no-build-isolation -e /work/generator/workspace`. MCIO and the block registry remain external runtime dependencies. On another machine, set `MCIO_ROOT` to the MCIO source directory and `MINECRAFT_STATES` to a copy of the authoritative `1.21.1-blocks.json`. Rendering additionally uses the supplied MCRender installation and Xvfb. Generation and validation do not require the renderer or a GPU. ## A short independent client ```python from pathlib import Path from hearthwright import ( Attachment, Choice, Design, Join, Level, Materials, Roof, Site, Span, Volume, generate, ) spec = Design( "Birchwater workshop", volumes=( Volume("home", width=Span(13, 17, 2), depth=15, levels=(Level(("living", "kitchen")), Level(("sleeping", "study"), attic=True)), roof=Roof(ridge=Choice("x", "z"), pitch=1.0)), Volume("work", width=9, depth=11, join=Join("home", side="east", align=Choice("front", "back")), levels=(Level(("workshop", "storage")),), roof=Roof(form="hip", pitch=.5)), ), materials=Materials(wall="plaster", frame="spruce", roof="sandstone"), site=Site(planting="temperate"), attachments=(Attachment("porch", "home", side="south", width=7, depth=3, cover="pergola"),), ) building = generate(spec, seed=37) building.validate().require_valid() building.export(Path("workshop.litematic")) building.verify(Path("workshop.litematic")) ``` Ordinary clients need no absolute coordinates, placement loops, blockstates, geometry repairs or NBT code. The library also works without the CLI: ```python from hearthwright import Building, resolve, validate plan = resolve(spec, seed=37) # Inspect choices before allocating blocks. print(plan.summary()) print(plan.signature()) report = validate(Path("workshop.litematic")) restored = Building.load(Path("workshop.litematic")) assert restored.validate().valid ``` ## Architectural API and domains All declaration classes are frozen dataclasses. Parameters marked **sampled** accept a concrete value, `Choice(a, b, ...)`, or, for integers, `Span(low, high, step=1)`. A span is inclusive at its upper bound; values advance from `low` by `step`. A choice is uniform over currently feasible alternatives. Concrete values remain requirements. | Declaration | Parameters and dependencies | | --- | --- | | `Design(name, volumes, ...)` | One dwelling or property. Supply ordered volumes, material roles, site, optional entrance, attachments, features and resource limits. Names identify components and joins; they never select implementations. | | `Volume(name, width, depth, levels, ...)` | Sampled outer width 9..25 and depth 9..23, tightened by the actual insets and circulation. Every additional mass needs a `Join` to an earlier mass. | | `Volume.levels` | A tuple of 1..4 `Level` declarations, or `Choice` of such tuples. This permits real seeded floor-count and program changes. Root envelopes allow at most two levels. | | `Level(rooms, layout, height, inset, wall, attic)` | `rooms` is a tuple of functions. Sampled `layout`: `open`, `cross` (split across width), or `long` (split across depth). Split layouts need at least two functions and at least 15 blocks on the splitting axis. Programs divide between two actual rooms with a connecting door. Regular floor spacing is 5..7. | | Level dimensions | `inset` 0..3 shrinks that level relative to its volume. Every resulting floor is at least 9 by 9 before the wall thickness. An upper floor wider than the lower receives posts, beams and knee braces automatically. An attic must be last and has a three-block knee-wall zone beneath its sloping roof. Its declared height is not an extra occupied floor. | | Room functions | `living`, `kitchen`, `sleeping`, `storage`, `workshop`, `study`, `enchanting`, `brewing`, `trading`, `fishing`. Furniture size responds to available room area. Multiple functions can share an open room. Excessive programs that cannot fit with clear access receive a diagnostic rather than losing functions. | | `Join(to, side, align, level)` | Sampled cardinal `side`; sampled `front`, `center`, or `back` alignment along the shared edge. `level` selects the target storey, and the wing inherits its floor elevation. `front` means the low coordinate end of that edge, independent of the entrance direction. Joined masses share a real doorway, not a declared graph edge alone. | | Vertical circulation | Sampled `stairs` or `ladder`. Straight stairs need common depth at least lower-storey height + 6 and common width at least 11, plus both landings and clearance from openings. Successive stair flights alternate sides. A ladder needs a shared backed shaft and a clear exit. Concrete stairs never silently become ladders. | | `Roof(form, ridge, pitch, eaves, dormers, cupola)` | Sampled `gable`/`hip`; sampled `x`/`z`/`long`/`short` ridge; sampled pitch .5, 1.0 or 1.5; eaves 1 or 2. Long/short resolve from the mass proportions. All roofs have solid backing and filled risers. | | Attics and dormers | Attics require pitch >= 1. Dormers are sampled 0..2, require a gable with pitch >= 1, and two require ridge length >= 17. Their integrated height field closes valleys and side transitions. Cupolas are sampled booleans and form sealed, solid-cored ridge lanterns, not inaccessible occupied rooms. Use a joined multi-level volume for a usable tower. | | Root envelope | `envelope="roots", roof=None`, sampled `root_spread` 3..6 and `root_crown` 3..7. A mound, tapering trunk, swept oriented roots and hooked crown surround carved, chamfered rooms. The dark cap seals into the trunk. Openings receive daylight tunnels through the hill. Root levels use open programs, not rectangular partitions. | | Foundations and water | `raised` 0..7 raises a root volume's base above the normal floor datum. `supports="foundation"` fills a continuous foundation; `"posts"` drives piles to the ground or bed. Here “root volume” means the first mass in the join graph, regardless of its envelope. Joined volumes inherit elevation and must omit `raised`. Water sites require posts and an initial raised value >= water depth + 1. | | `Entrance(volume, side, level, bay)` | Cardinal side and an existing occupied level. Sampled `left`, `center`, or `right` bay. The exterior approach uses the real entry height and connects through any matching terrace/deck. Default: first volume, south side, ground floor, center bay. | | `Attachment(kind, to, ...)` | `porch`, `deck`, `terrace`, `balcony`, `market`; cardinal sampled side, occupied level, sampled depth 3..7, sampled width `full` or 5..facade span. Cover is `open`, `pergola` or `canopy`. Every attachment receives a door, supports, circulation, furniture and light. Pergolas have spaced rafters with open sky between them. | | `Feature(kind, side, scale)` | `pool`, `field`, `well`, `windmill`, `landing`, `garden`. Sampled side and scale 5..11. Windmills require a plot at least 9 wide, including their sail clearance. Plots are allocated outside the complete massing and attached floors. Walks avoid buildings, approach stairs and other feature plots. Landings on dry sites excavate a contained water reach. | | `Site(terrain, margin, planting, rise, water_depth, paving)` | Terrain `meadow`, `slope`, `water`, `hill`; margin 3..12; rise 2..7; shallow water depth 1..3. Planting `temperate`, `mediterranean`, `conifer`, `wetland`, `sparse`. Paving `garden`, `courtyard`, `yard`; water uses garden plus supported attachments. A slope rises behind the south approach and is excavated out of all inhabited cells. | | `Materials` | Wall roles: `stone`, `masonry`, `plaster`, `white`, `blue`, `timber`, `earth`. Roof roles: `pale_stone`, `sandstone`, `slate`, `gray`, `red`, `tile`, `dark_wood`. Frame/floor use vanilla wood roles; trim uses wood or sandstone. Accent is one of the 16 vanilla colors. Shutters can be disabled. Role palettes resolve to validated blocks, including distinct full backing, stair and slab materials. | | `Limits` | Default horizontal extent 128, vertical extent 96, volume 1,500,000 cells, and 300,000 non-air blocks. Configure these explicitly for larger coherent properties. The library derives actual bounds, including roots, sails, trees and terrain, and rejects excesses; it never clips blocks. | The references' scale is intentional. A width parameter is bounded; it is not a route to arbitrarily large halls with the same tiny furnishings. Large rooms receive dining groups, wardrobes, longer work counters, shelving and additional lamps; compact rooms use smaller functional groups. Dependencies are resolved before geometry where possible. The planner backtracks within declared finite domains when an earlier choice causes a later conflict. It examines at most 128 candidates by default; `resolve(..., max_candidates=N)` exposes that planning budget. It returns an actionable error for contradictions or budget exhaustion, without switching to a fixed fallback building. A user can reduce conflicting choice domains or relocate a wing. Construction errors such as an overfull room program remain errors; generation does not suppress failed validators or repair a particular seed by hand. ## Reproduction and records Seeds are signed 64-bit integers, including zero and negative values. All sampling, furnishing choices, terrain texture and plants use seeded `random.Random` instances. Geometry ordering is deterministic. There are no time-based seeds, global random calls, network calls or seed-specific exceptions. The same declaration and seed produce the same geometry and bytes in the supplied environment. Python's seed stream can map distinct seeds to the same decisions; that is not evidence of additional structural variety. Export writes: - One `.litematic` region, Java data version 3955, with zeroed creation/modification timestamps. - An adjacent `.json` sidecar: the original specification, seed, resolved masses and room layouts, roof choices, component coordinates, protected routes, expected openings, support paths, furniture, light locations, inventories, actual bounds and structural signatures. The sidecar uses world coordinates, which can be negative. `origin` maps them into positive schematic coordinates. `Building.load(path, metadata=...)` accepts the adjacent sidecar by default, a metadata dictionary, or an explicit sidecar path. Keep the sidecar with the export for architecture-aware validation. The Litematic remains independently usable in Minecraft. `verify(path)` compares the entire non-air coordinate set and every final blockstate after reload. It uses MCIO's parsed block names and explicit property mappings. Property order is ignored; changed names, values or omitted properties still fail. Multiple writes to a cell are valid: the final state is what is checked. Actual container NBT is reloaded and checked against the documented supplies, rather than trusting the JSON alone. ## Validation and diagnostics `generate(...)` validates by default; `Building.export(...)` validates again before writing. `generate(..., validate=False)` is intended for rule development and diagnostics, not for publishing an unchecked sample. `validate` returns a `Report` and does not throw on ordinary geometry failures. `report.require_valid()` raises `ValidationError`, retaining the full report. ```python report = building.validate() for issue in report.diagnostics: print(issue.rule, issue.component, issue.location, issue.message) ``` Representative rules: | Rule | Actual evidence checked | | --- | --- | | `block.state`, `bounds.*` | Every used name/property/value against the supplied 1.21.1 registry; final geometry and recorded export bounds/resource limits. | | `multiblock.door`, `multiblock.bed` | Matching halves or adjacent heads/feet, directions, explicit properties and support. | | `support.column`, `.stair`, `.ladder`, `.light` | Unbroken recorded load paths; supported treads; directionally backed ladder rungs; attached lanterns. Plant and crop substrates are also inspected. | | `clearance.route`, `support.route` | Actual foot/head blocks for every protected passage and actual floor support. | | `reachability.*` | Voxel traversal from the exterior start through the final geometry to every room anchor, outdoor working target and furniture interaction point. Rising movement requires a suitably directed stair or ladder; ordinary one-block jumps cannot mask a broken staircase. | | `envelope.leak`, `.intrusion` | Flooding of actual interior voids against the envelope, with closed doors as weather seals. Full roof backing is required around partial stair/slab models. Water and hill material inside rooms are rejected. | | `opening.glazing`, `.clearance` | The expected glazing is still glass and room-side reveals are clear. | | `program.missing`, `furnishing.changed`, `lighting.room` | Every assigned function has realized furniture, final furniture states remain present, and every room has an actual near-floor light. | | `inventory.changed` | Reloaded Java block-entity contents match the supplies recorded for each container. | Reservations protect the paths derived from empty, already constructed geometry before furniture placement. Furniture candidates must have actual support and a reachable interaction cell; their access routes become reservations before the group is placed. Final validation reruns traversal after furnishing, planting, fence connection updates and all other passes. A declared room connection is never accepted as proof of reachability. The navigation model is conservative and tailored to the emitted vocabulary. Closed wooden doors are treated as operable; locked doors, jumping puzzles, swimming routes and redstone doors are outside the contract. This is not a Minecraft engine or a proof of every possible client specification. Weather validation treats full cubes and closed doors as seals and requires solid backing behind partial roofs. Light placement is validated; exact Minecraft light propagation, mob spawning and renderer brightness are not simulated. ## Implementation and extension points The modules have separate responsibilities: - `api.py`: immutable declarations and bounded domains. - `plan.py`: relative spatial resolution, constrained choice search, floor masks and plot allocation. - `build.py`: shared construction pipeline, openings, circulation and artifact API. - `roofs.py`: backed gable/hip height fields, roof unions, dormers, ridge lanterns and root sweeps. - `site.py`: terrain, supports, outdoor floors, property features, walks and planting. - `furnish.py`: function-to-furniture rules and placement around reservations. - `navigation.py`: geometry-based movement and path recovery. - `blocks.py`: authoritative blockstate construction, sparse geometry, NBT and deterministic transport. - `validate.py`: independent diagnostic rules over in-memory or reloaded geometry. - `components.py`: reusable component extension contracts. `@validation_rule` registers an additional pure rule with signature `(grid, metadata, report)`. A rule calls `report.add(rule_id, component, message, location)` for failures. Existing generation/export validation invokes registered rules automatically. `register_site_component(SiteComponent(...))` adds a reusable site capability without modifying planning or dispatch. A component supplies its name, version, inclusive scale domain, `realize(context, resolved_feature)` and optional validation rule. The planner allocates its plot alongside built-in features and records its version. The renderer callback uses `context.g`, `context.ground` and the resolved `Rect`; it records its support columns and openings, reserves clear cells through `grid.reserve`/`grid.route`, sets `feature['target']`, and appends the matching metadata feature. Shared path planning then connects that target, and shared validation checks its realized access. The extension test demonstrates this contract with a tool cache. Reinstall/register the same component version when reproducing an extended design. New roof or envelope algorithms can follow the existing component pipeline: resolve a bounded footprint and interfaces, contribute interior/clearance masks, realize geometry before furniture, and add diagnostics over final cells. The current public roof vocabulary remains deliberately limited to the implemented, tested forms. Adding an unsupported string never silently approximates it. ## Reference-derived capabilities and new compositions All eight supplied images and generators were inspected. The source snapshots remain unchanged, with their provenance in `references/README.md`. Useful ideas were extracted into shared mechanisms rather than importing or executing those generators. | Reference | Capabilities exercised by the independent client | | --- | --- | | Mediterranean villa | Offset unequal-height wings, pale plaster and turquoise shutters, shallow tile roofs, two terrace levels, open pergolas, planted courtyard and pool. | | Merchant manor | Masonry undercroft, elevated public entrance, supported terrace, exterior stair, timber/plaster upper floors, dark roof, study tower and striped market canopy. | | Red-roof farmstead | Steep red roof with pale verges, inhabited attic, dormer, ridge lantern, working field, sheltered well and a separate windmill plot. | | Root house | Earth and stone mound, irregular hollow floor masks, broad swept dark roots, forked crown, sealed trunk cap, recessed entrance and stacked furnished rooms. | | Sandstone cottage | Broad pale roof with dark trim, connected projecting bay, excavated slope floor, elevated entrance and stairs, and a small waterside jetty. | | Stilt water cabin | Low overhanging timber roof, piles in shallow water, railed deck, supported descending approach, landing and stocked fishing/cooking quarters. | | Stilted manor | Recessed masonry ground floor, wider supported upper floor, long beams and braces, steep inhabited roof, dormers, ridge lantern and balcony. | | Stone cottage | Compact stone/timber footprint, restrained pale gable, recessed entry, clear glazing, planters, domestic main room and backed ladder loft. | The novel clients are a **canal apothecary** (piled plaster home, hipped roof, brewing wing and pergola), **woodland archive** (small dwelling plus a taller library wing), **orchard house** (sampled floor count, domestic loft, covered working terrace and field), and **hillside studio** (a hollow root dwelling joined to a conventional workshop). Their geometry is entirely generated by the same library. This is an architectural interpretation of the distribution, not an exact replica of any screenshot. Logos, text, presentation borders and shader effects are excluded. Boats are supplied as inventory items at moorings; the library does not place vehicle entities. Ridge lanterns and the small windmill are architectural features, not redstone machines. Container stock is modest. Exterior chimneys, curved roofs, arbitrary polygons and arbitrary-height stair flights are not current public capabilities. ## Evidence and review artifacts `samples/manifest.json` records the finite delivered sample batch, client paths, seeds, complete resolved decisions, semantic reload counts and validation reports. Two seeds per client change real dimensions, alignments, circulation, partitions, ridge directions, pitch, root morphology or floor counts. The manifest's observed counts describe this batch only. `test/test_corpus.py` validates twelve clients at twelve fixed seeds, including both signed seed boundaries, plus targeted size/pitch and interaction cases. Other tests check determinism, semantic property equality, supported custom components, contradictory specifications, and intentional damage to stairs, doors, beds, posts, ladders, glazing, roofs, lighting, furniture, inventories and bounds. Temporary artifacts live under `test/.tmp` and are removed. `scripts.audit` supplies a separately rerunnable consecutive-seed audit and records failures without hiding them. `previews/index.html` is an offline labeled review sheet. Each representative property has two exterior angles plus cutaways at the main volume's occupied floors. Cutaways are geometry-only review images; their open walls/roofs are not defects in the exported complete building. `render_batch.mjs` reuses the supplied renderer configuration; `render.mjs` remains a convenient four-view renderer for a single file. Tests and the finite seed audit provide sampled and targeted evidence. They do not exhaust the admitted parameter domains or establish a mathematical guarantee for every new composition. Invalid explicit relationships, excessive programs and exhausted planning budgets produce diagnostics so the client can narrow its requirements. The library does not claim unsupported combinations as available architecture. MD /opt/venv/bin/yapf -i hearthwright/*.py examples/*.py scripts/*.py generate.py /opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1

日志保留的输出末尾

#047 · 2026-09-25T15:50:58.028Z · exit=0 · tail -20 samples/audit.log && tail -8 samples/batch.log && rg -n 'points.update|s.floor - 2' hearthw
tail -20 samples/audit.log && tail -8 samples/batch.log && rg -n 'points.update|s.floor - 2' hearthwright/build.py

日志保留的输出末尾

mediterranean_villa: audited 64 seeds merchant_manor: audited 64 seeds red_roof_farmstead: audited 64 seeds root_house: audited 64 seeds sandstone_cottage: audited 64 seeds stilt_water_cabin: audited 64 seeds stilted_manor: audited 64 seeds stone_cottage: audited 64 seeds canal_apothecary: audited 64 seeds woodland_archive: audited 64 seeds orchard_house: audited 64 seeds hillside_studio: audited 64 seeds canal_apothecary seed=0: 7136 blocks, validated after reload canal_apothecary seed=1: 6672 blocks, validated after reload woodland_archive seed=0: 5011 blocks, validated after reload woodland_archive seed=1: 4467 blocks, validated after reload orchard_house seed=0: 4866 blocks, validated after reload orchard_house seed=1: 4778 blocks, validated after reload hillside_studio seed=0: 5342 blocks, validated after reload hillside_studio seed=1: 4812 blocks, validated after reload 125: points.update((x, z) for x in (r.x0, r.x1) for z in (r.z0 + r.depth // 3, r.z1 - r.depth // 3)) 133: g.stair(x + dx, s.floor - 2, z + dz, mat.floor, next(k for k, delta in DIRECTIONS.items() if delta == (-dx, -dz)), 'top')
#048 · 2026-09-25T15:51:56.294Z · exit=0 · /opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1
/opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1

日志保留的输出末尾

#049 · 2026-09-25T15:52:02.196Z · exit=0 · cat >> test/test_roundtrip.py <<'PY'
cat >> test/test_roundtrip.py <<'PY' def test_harness_client_runs_from_another_directory(artifact_dir): import subprocess import sys root = Path(__file__).resolve().parents[1] subprocess.run([sys.executable, str(root / 'generate.py'), '--seed', '73'], cwd=artifact_dir, check=True, capture_output=True) default_path = artifact_dir / 'output.litematic' assert default_path.exists() assert validate(default_path).valid second = artifact_dir / 'explicit.litematic' subprocess.run([sys.executable, str(root / 'generate.py'), '--seed', '73', '--output', str(second)], cwd=artifact_dir, check=True, capture_output=True) assert default_path.read_bytes() == second.read_bytes() PY /opt/venv/bin/yapf -i test/test_roundtrip.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1 /opt/venv/bin/python generate.py

日志保留的输出末尾

wrote output.litematic: {'blocks': 6462, 'states': 79, 'size_xyz': [38, 35, 36]}; validation passed
#050 · 2026-09-25T15:52:51.952Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/build.py';s=open(p).read().replace('from .blocks import Grid, is_air, passable, name', 'from .blocks import Grid, is_air, passable, name, full_support, split_state') s=s.replace(' self.reserve_rooms()\n from .furnish', ' self.reserve_rooms()\n structural_geometry = self.geometry_signature()\n from .furnish').replace("self.g.metadata['geometry_signature'] = self.geometry_signature()", "self.g.metadata['geometry_signature'] = structural_geometry") a=s.index(' role = ',s.index(' def geometry_signature'));b=s.index(' if role:',a) s=s[:a]+""" role = None if n.endswith('_door') or n.endswith('_stairs') or n == 'ladder': properties = split_state(state)[1] role = ('door' if n.endswith('_door') else 'stair' if n.endswith('_stairs') else 'ladder') + ':' + properties.get('facing', '') + ':' + properties.get('half', '') elif 'glass' in n: role = 'glass' elif n.endswith('_slab'): role = 'slab:' + split_state(state)[1].get('type', '') elif full_support(state): role = 'solid' """+s[b:] s=s.replace(' # Geometry classifications retain roof/stair topology, floor masks and partitions.', ' # Snapshot before furnishing and planting; normalize all structural material names.') open(p,'w').write(s) PY cat >> test/test_corpus.py <<'PY' def test_structural_signatures_exclude_material_noise_and_furniture(): a = Design('A', (Volume('home', width=13, depth=13),), materials=Materials(wall='stone', roof='slate')) b = replace(a, name='B', materials=Materials(wall='plaster', roof='red', accent='blue')) first, second = generate(a, 11), generate(b, 11) assert first.grid.blocks != second.grid.blocks assert first.metadata['structural_signature'] == second.metadata['structural_signature'] assert first.metadata['geometry_signature'] == second.metadata['geometry_signature'] PY /opt/venv/bin/yapf -i hearthwright/build.py test/test_corpus.py /opt/venv/bin/python -m pytest -q test/test_corpus.py::test_structural_signatures_exclude_material_noise_and_furniture /opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1

日志保留的输出末尾

. [100%] 1 passed in 0.39s
#051 · 2026-09-25T15:52:57.026Z · exit=0 · tail -6 test-results.txt && /opt/venv/bin/python - <<'PY'
tail -6 test-results.txt && /opt/venv/bin/python - <<'PY' import json from pathlib import Path manifest=json.loads(Path('samples/manifest.json').read_text()) print('samples',len(manifest['samples']), 'observed',manifest['observed_structures'],manifest['observed_geometry_signatures']) a=json.loads(Path('samples/audit.json').read_text());print('audit',len(a['clients'])*len(a['seeds']),'failures',len(a['failures'])) PY

日志保留的输出末尾

........................................................................ [ 36%] ........................................................................ [ 73%] .................................................... [100%] 196 passed in 34.75s samples 24 observed 24 24 audit 768 failures 0
#052 · 2026-09-25T15:54:31.470Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/build.py';s=open(p).read();a=s.index(" for y in range(ceiling, ceiling + 2 + depth // 2):");b=s.index(' else:',a) s=s[:a]+""" cap = ceiling + 1 + min(3, depth) for y in range(ceiling, cap): if is_air(g.get(x, y, z)) or y == ceiling: g.put(x, y, z, 'dark_oak_log', axis='y') if depth < 2 and (x + 2 * z) % 5 <= 1 and is_air(g.get(x, cap, z)): g.put(x, cap, z, 'moss_block') """+s[b:];open(p,'w').write(s) p='hearthwright/furnish.py';s=open(p).read();needle=' return programs[function]';repl=""" if area >= 150 and function == 'living': stove = [(x, y, 0, 'stone_bricks', {}) for x in (0, 2) for y in (0, 1)] stove += [(1, 0, 0, 'furnace', {'facing': 'south'}), (1, 1, 0, 'chiseled_stone_bricks', {})] stove += [(x, 2, 0, 'stone_brick_slab', {'type': 'top'}) for x in range(3)] programs[function].append(('masonry_stove', stove)) programs[function].append(('sideboard', [(0, 0, 0, 'barrel', {'facing': 'south'}), (1, 0, 0, 'bookshelf', {}), (1, 1, 0, 'potted_fern', {})])) return programs[function]""";s=s.replace(needle,repl);open(p,'w').write(s) PY /opt/venv/bin/yapf -i hearthwright/build.py hearthwright/furnish.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#053 · 2026-09-25T15:55:20.135Z · exit=0 · /opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1
/opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1

日志保留的输出末尾

#054 · 2026-09-25T15:55:26.576Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' import importlib from scripts.sample_batch import CLIENTS from hearthwright import generate from hearthwright.navigation import flood,walkable for client in CLIENTS: b=generate(importlib.import_module('examples.'+client).DESIGN,0) reached=flood(b.grid,b.metadata['start']) misses=[] for r in b.metadata['rooms']: unreachable=[(x,r['floor']+1,z) for x,z in r['cells'] if walkable(b.grid,(x,r['floor']+1,z)) and (x,r['floor']+1,z) not in reached] if unreachable:misses.append((r['id'],len(unreachable),unreachable[:3])) print(client,misses) PY

日志保留的输出末尾

mediterranean_villa [] merchant_manor [] red_roof_farmstead [] root_house [] sandstone_cottage [] stilt_water_cabin [] stilted_manor [] stone_cottage [] canal_apothecary [] woodland_archive [] orchard_house [] hillside_studio []
#055 · 2026-09-25T15:55:32.179Z · exit=0 · tail -4 test-results.txt && tail -5 samples/audit.log
tail -4 test-results.txt && tail -5 samples/audit.log

日志保留的输出末尾

........................................................................ [ 36%] ........................................................................ [ 73%] ..................................................... [100%] 197 passed in 29.02s stone_cottage: audited 64 seeds canal_apothecary: audited 64 seeds woodland_archive: audited 64 seeds orchard_house: audited 64 seeds hillside_studio: audited 64 seeds
#056 · 2026-09-25T15:56:54.109Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/furnish.py';s=open(p).read().replace(" candidates = [(x, z, rot)", " original_reachable = set(flood(g, room['anchor'], allowed, maximum=2000))\n candidates = [(x, z, rot)") needle=" contacts = [(px + dx, floor + 1, pz + dz)" pos=s.index(needle) s=s[:pos]+" removed = {(px, floor + 1, pz) for px, pz in footprint}\n if original_reachable - removed - set(parents):\n continue\n"+s[pos:];open(p,'w').write(s) p='hearthwright/validate.py';s=open(p).read().replace('from .components import SITE_COMPONENTS', 'from .components import SITE_COMPONENTS, BUILTIN_FEATURES') s=s.replace(" report.checked['blocks'] = len(g.blocks)", " for feature in m.get('resolved', {}).get('features', []):\n kind = feature['kind']\n if kind not in BUILTIN_FEATURES:\n if kind not in SITE_COMPONENTS:\n report.add('component.missing', kind, 'register the recorded site component to run its validation rules')\n elif feature.get('version') != SITE_COMPONENTS[kind].version:\n report.add('component.version', kind, 'registered component version differs from the exported design')\n report.checked['blocks'] = len(g.blocks)") needle=" if tuple(room['anchor']) not in reachable:\n report.add('reachability.room', room['id'], 'room is unreachable from the exterior start', room['anchor'])" repl=needle+"\n for x, z in room['cells']:\n p = (x, room['floor'] + 1, z)\n if walkable(g, p) and p not in reachable:\n report.add('reachability.floor', room['id'], 'usable floor area is isolated from the entrance', p)\n break" s=s.replace(needle,repl);open(p,'w').write(s) PY cat >> test/test_validation.py <<'PY' def test_isolated_usable_floor_is_detected_even_when_anchor_remains_reachable(): from hearthwright import Level from hearthwright.navigation import walkable from hearthwright.materials import DIRECTIONS b = generate(Design('Floor pocket', (Volume('home', width=13, depth=13, levels=(Level(('living',)),)),)), 7) room = b.metadata['rooms'][0] choices = [(x, room['floor'] + 1, z) for x, z in room['cells'] if walkable(b.grid, (x, room['floor'] + 1, z))] p = max(choices, key=lambda p: abs(p[0] - room['anchor'][0]) + abs(p[2] - room['anchor'][2])) for dx, dz in DIRECTIONS.values(): b.grid.box((p[0] + dx, p[1], p[2] + dz, p[0] + dx, p[1] + 1, p[2] + dz), 'stone') assert 'reachability.floor' in rules(b) PY /opt/venv/bin/yapf -i hearthwright/furnish.py hearthwright/validate.py test/test_validation.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#057 · 2026-09-25T15:57:32.910Z · exit=0 · cat > examples/courtyard_house.py <<'PY'
cat > examples/courtyard_house.py <<'PY' # A new composition: three connected masses frame a sheltered entrance court. from hearthwright import Attachment, Choice, Design, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Three gables court', volumes=( Volume('hall', width=Choice(23, 25), depth=Choice(11, 13), levels=(Level(('living', 'kitchen', 'study')), Level(('sleeping', 'storage'), layout='cross')), roof=Roof(ridge='x', pitch=.5)), Volume('work', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'front'), levels=(Level(('workshop', 'storage')),), roof=Roof(ridge='z', pitch=1.0)), Volume('guest', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'back'), levels=(Level(('sleeping', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='plaster', frame='spruce', roof='slate', accent='blue'), site=Site(margin=3, planting='sparse', paving='yard'), attachments=(Attachment('porch', 'hall', width=5, depth=3, cover='pergola'),), ) if __name__ == '__main__': run(DESIGN) PY /opt/venv/bin/python - <<'PY' from hearthwright import generate from examples.courtyard_house import DESIGN for seed in (0,1,17): try: b=generate(DESIGN,seed);print(seed,'valid',len(b.grid.blocks)) except Exception as exc:print(seed,str(exc)) PY

日志保留的输出末尾

0 valid 6656 1 valid 5832 17 valid 6782
#058 · 2026-09-25T15:59:07.907Z · exit=1 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='scripts/sample_batch.py';s=open(p).read().replace("'hillside_studio',", "'hillside_studio', 'courtyard_house',");open(p,'w').write(s) p='hearthwright/validate.py';s=open(p).read();needle=" for f in m['furniture']:\n for p, expected in zip(f['cells'], f['states']):";repl=""" for feature in m['features']: if not any(light['owner'] == feature['owner'] and name(g.get(*light['point'])) in ('lantern', 'glowstone', 'sea_lantern') for light in m['lights']): report.add('lighting.site', feature['owner'], 'important outdoor space lacks its actual light', feature['target']) for owner in ('entrance', 'approach'): if not any(light['owner'] == owner and name(g.get(*light['point'])) == 'lantern' for light in m['lights']): report.add('lighting.approach', owner, 'public approach needs an actual light') for f in m['furniture']: for p, expected in zip(f['cells'], f['states']):""";s=s.replace(needle,repl);open(p,'w').write(s) p='test/test_api.py';s=open(p).read().replace(" ctx.g.put(x, y + 1, z, 'barrel')", " ctx.g.put(x, y + 1, z, 'barrel')\n ctx.g.lantern(x, y + 2, z, feature['id'])");open(p,'w').write(s) p='GENERATOR.md';s=open(p).read().replace('four new compositions', 'five new compositions').replace('All twelve independent clients', 'All thirteen independent clients').replace('`test/test_corpus.py` validates twelve clients', '`test/test_corpus.py` validates thirteen clients') s=s.replace('and **hillside studio** (a hollow root dwelling joined to a conventional workshop).', '**hillside studio** (a hollow root dwelling joined to a conventional workshop), and **courtyard house** (three connected masses around a sheltered entrance court).') s=s.replace('`raised` 0..7 raises a root volume\'s base', '`raised` 0..7 raises the first volume\'s base').replace('Here “root volume” means the first mass in the join graph, regardless of its envelope. ', '') s=s.replace('to every room anchor, outdoor working target and furniture interaction point.', 'to every room anchor, every usable cell at its floor, outdoor working target and furniture interaction point.') s=s.replace('sets `feature[\'target\']`,', 'records a light owned by the feature, sets `feature[\'target\']`,') s=s.replace('Furniture candidates must have actual support and a reachable interaction cell;', 'Furniture candidates must have actual support and a reachable interaction cell and must preserve the connected usable floor area;') open(p,'w').write(s) PY /opt/venv/bin/yapf -i hearthwright/validate.py test/test_api.py scripts/sample_batch.py examples/courtyard_house.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#059 · 2026-09-25T16:00:01.007Z · exit=0 · tail -5 test-results.txt && tail -4 samples/audit.log
tail -5 test-results.txt && tail -4 samples/audit.log

日志保留的输出末尾

FAILED test/test_corpus.py::test_diversity_is_structural_and_realized - heart... FAILED test/test_corpus.py::test_root_envelope_is_nonrectangular_and_buried_floor_excavated FAILED test/test_roundtrip.py::test_reloaded_samples_validate_with_explicit_metadata[courtyard_house] FAILED test/test_validation.py::test_stair_facing_and_tread_support_are_rejected 31 failed, 180 passed in 35.33s stilted_manor: audited 64 seeds stone_cottage: audited 64 seeds canal_apothecary: audited 64 seeds woodland_archive: audited 64 seeds
#060 · 2026-09-25T16:00:08.518Z · exit=0 · rg '^E +[a-z].*|^FAILED|^FAILED|^F[A-Z]+|^.*lighting\.|^.*reachability.floor' test-results.txt sampl
rg '^E +[a-z].*|^FAILED|^FAILED|^F[A-Z]+|^.*lighting\.|^.*reachability.floor' test-results.txt samples/audit.log | head -65

日志保留的输出末尾

log:FAILED red_roof_farmstead seed=40: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=41: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=42: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=43: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=44: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=49: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=50: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=53: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=54: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=55: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=57: 1 architectural validation failures samples/audit.log:lighting.approach: approach : public approach needs an actual light samples/audit.log:FAILED red_roof_farmstead seed=58: 1 architectural validation failures
#061 · 2026-09-25T16:00:23.311Z · exit=1 · /opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1
/opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1

日志保留的输出末尾

#062 · 2026-09-25T16:00:36.866Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from collections import Counter s=open('test-results.txt').read().splitlines() print(Counter(line.split(':')[0] for line in s if line.startswith('E ') and ': ' in line)) PY

日志保留的输出末尾

Counter({'E lighting.approach': 38, 'E hearthwright.validate.ValidationError': 31})
#063 · 2026-09-25T16:01:19.304Z · exit=0 · rg 'E +lighting.approach:' test-results.txt | sort | uniq -c
rg 'E +lighting.approach:' test-results.txt | sort | uniq -c

日志保留的输出末尾

25 E lighting.approach: approach : public approach needs an actual light 13 E lighting.approach: entrance : public approach needs an actual light
#064 · 2026-09-25T16:03:02.883Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' p='hearthwright/site.py';s=open(p).read();a=s.index(' for d in (2, length):',s.index('def approach(ctx)'));b=s.index('\n\ndef feature',a) s=s[:a]+''' def approach_lighting(ctx): """Place public path lamps after path construction, outside all reserved passages.""" g, p = ctx.g, ctx.plan sx, sy, sz = ctx.start candidates = [(sx + dx, sz + dz) for dx in range(-6, 7) for dz in range(-6, 7) if 2 <= abs(dx) + abs(dz) <= 6] candidates.sort(key=lambda q: (abs(q[0] - sx) + abs(q[1] - sz), q)) installed = [] for x, z in candidates: if not p.site.inset(1).contains(x, z) or any(abs(x - px) + abs(z - pz) < 4 for px, pz in installed): continue y = p.design.site.water_depth + 1 if p.design.site.terrain == 'water' else ctx.ground.get((x, z), 1) if abs(y + 2 - sy) > 4 or not g.free([(x, y + 1, z), (x, y + 2, z)]): continue if p.design.site.terrain == 'water': column(ctx, x, z, y, 'approach-light') elif not full_support(g.get(x, y, z)): continue g.put(x, y + 1, z, 'stone_brick_wall') g.lantern(x, y + 2, z, 'approach') installed.append((x, z)) if len(installed) == 2: return if not installed: raise ValueError('no clear site edge for an approach lamp') ''' +s[b:] needle=" candidates = [(x, z) for x, z in sorted(p.site.inset(3).cells())" s=s.replace(needle," for light in g.metadata['lights']:\n x, _, z = light['point']\n forbidden |= {(x + dx, z + dz) for dx in range(-2, 3) for dz in range(-2, 3)}\n"+needle) open(p,'w').write(s) p='hearthwright/build.py';s=open(p).read().replace(' site.connect_features(self)\n self.reserve_rooms()', ' site.connect_features(self)\n site.approach_lighting(self)\n self.reserve_rooms()');open(p,'w').write(s) p='hearthwright/validate.py';s=open(p).read();a=s.index(" for owner in ('entrance', 'approach'):");b=s.index(" for f in m['furniture']:",a) s=s[:a]+""" for owner, target in (('entrance', m['entrance']), ('approach', m['start'])): nearby = [light for light in m['lights'] if name(g.get(*light['point'])) == 'lantern' and sum(abs(a - b) for a, b in zip(light['point'], target)) <= 10 and abs(light['point'][1] - target[1]) <= 4] if not nearby: report.add('lighting.approach', owner, 'public approach needs an actual nearby light', target) """+s[b:];open(p,'w').write(s) p='hearthwright/api.py';s=open(p).read().replace(" if self.low > self.high or self.step < 1:\n raise ValueError('Span requires low <= high and step >= 1')", " if any(type(v) is not int for v in (self.low, self.high, self.step)) or self.low > self.high or self.step < 1:\n raise ValueError('Span requires integer low <= high and step >= 1')\n if len(range(self.low, self.high + 1, self.step)) > 256:\n raise ValueError('Span supports at most 256 declared values; narrow the domain')") open(p,'w').write(s) p='GENERATOR.md';s=open(p).read().replace('A span is inclusive at its upper bound;', 'Spans contain at most 256 declared values to bound sampling resources. A span is inclusive at its upper bound;').replace('Every attachment receives a door, supports, circulation, furniture and light.', 'Every attachment receives a door, supports, circulation, furniture and light. Nearby attachment lamps can also serve the entrance.');open(p,'w').write(s) PY /opt/venv/bin/yapf -i hearthwright/api.py hearthwright/site.py hearthwright/build.py hearthwright/validate.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#065 · 2026-09-25T16:04:50.058Z · exit=0 · tail -35 test-results.txt
tail -35 test-results.txt

日志保留的输出末尾

........................................................................ [ 34%] ........................................................................ [ 68%] ................................................................... [100%] 211 passed in 37.93s
#066 · 2026-09-25T16:05:00.725Z · exit=0 · cat scripts/render_batch.py && cat scripts/audit.py && cat scripts/sample_batch.py
cat scripts/render_batch.py && cat scripts/audit.py && cat scripts/sample_batch.py

日志保留的输出末尾

ing, generate CLIENTS = ( 'mediterranean_villa', 'merchant_manor', 'red_roof_farmstead', 'root_house', 'sandstone_cottage', 'stilt_water_cabin', 'stilted_manor', 'stone_cottage', 'canal_apothecary', 'woodland_archive', 'orchard_house', 'hillside_studio', 'courtyard_house', ) def main(): """Generate only a finite declared corpus, never enumerate parameter products.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--seeds', type=int, nargs='+', default=[0, 1]) parser.add_argument('--output-dir', type=Path, default=Path('samples')) args = parser.parse_args() records = [] for client in CLIENTS: design = importlib.import_module('examples.' + client).DESIGN for seed in args.seeds: building = generate(design, seed) path = args.output_dir / f'{client}-{seed}.litematic' building.export(path) roundtrip = building.verify(path) report = Building.load(path).validate().require_valid() records.append({'client': f'examples/{client}.py', 'seed': seed, 'schematic': path.name, 'metadata': path.with_suffix('.json').name, 'resolved': building.plan.summary(), 'structural_signature': building.metadata['structural_signature'], 'geometry_signature': building.metadata['geometry_signature'], 'validation': report.to_dict(), 'roundtrip': roundtrip}) print(f'{client} seed={seed}: {roundtrip["blocks"]} blocks, validated after reload', flush=True) manifest = {'schema': 1, 'command': 'python -m scripts.sample_batch', 'samples': records, 'observed_structures': len({r['structural_signature'] for r in records}), 'observed_geometry_signatures': len({r['geometry_signature'] for r in records})} args.output_dir.mkdir(parents=True, exist_ok=True) (args.output_dir / 'manifest.json').write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n') if __name__ == '__main__': main()
#067 · 2026-09-25T16:05:01.471Z · exit=0 · /opt/venv/bin/python generate.py
/opt/venv/bin/python generate.py

日志保留的输出末尾

wrote output.litematic: {'blocks': 6480, 'states': 82, 'size_xyz': [38, 35, 36]}; validation passed
#068 · 2026-09-25T16:05:08.331Z · exit=0 · /opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1
/opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1

日志保留的输出末尾

#069 · 2026-09-25T16:05:08.609Z · exit=0 · cat GENERATOR.md
cat GENERATOR.md

日志保留的输出末尾

re architectural features, not redstone machines. Container stock is modest. Exterior chimneys, curved roofs, arbitrary polygons and arbitrary-height stair flights are not current public capabilities. ## Evidence and review artifacts `samples/manifest.json` records the finite delivered sample batch, client paths, seeds, complete resolved decisions, semantic reload counts and validation reports. Two seeds per client change real dimensions, alignments, circulation, partitions, ridge directions, pitch, root morphology or floor counts. The manifest's observed counts describe this batch only. `test/test_corpus.py` validates thirteen clients at twelve fixed seeds, including both signed seed boundaries, plus targeted size/pitch and interaction cases. Other tests check determinism, semantic property equality, supported custom components, contradictory specifications, and intentional damage to stairs, doors, beds, posts, ladders, glazing, roofs, lighting, furniture, inventories and bounds. Temporary artifacts live under `test/.tmp` and are removed. `scripts.audit` supplies a separately rerunnable consecutive-seed audit and records failures without hiding them. `previews/index.html` is an offline labeled review sheet. Each representative property has two exterior angles plus cutaways at the main volume's occupied floors. Cutaways are geometry-only review images; their open walls/roofs are not defects in the exported complete building. `render_batch.mjs` reuses the supplied renderer configuration; `render.mjs` remains a convenient four-view renderer for a single file. Tests and the finite seed audit provide sampled and targeted evidence. They do not exhaust the admitted parameter domains or establish a mathematical guarantee for every new composition. Invalid explicit relationships, excessive programs and exhausted planning budgets produce diagnostics so the client can narrow its requirements. The library does not claim unsupported combinations as available architecture.
#070 · 2026-09-25T16:05:08.655Z · exit=0 · cat render_batch.mjs && cat /work/AGENTS.md && cat references/README.md
cat render_batch.mjs && cat /work/AGENTS.md && cat references/README.md

日志保留的输出末尾

ad.jpg` | `runs/exp8/red-roof-farmstead/work/generator/workspace/reference_red_roof_farmstead.jpg` | `cecfd8ad0047913471e105c5065cce44146779ff2a1aa7631ac7e452a51fa2fb` | | `references/root-house/generate.py` | `runs/exp8/root-house/work/generator/workspace/generate.py` | `d24c54dfa3d2bd1f7b2d6e485c9bb555d459365e51eb56811147cec108042d24` | | `reference_root_house.jpg` | `runs/exp8/root-house/work/generator/workspace/reference_root_house.jpg` | `65d82aa169f29a14feb45609180a75eae6f6a3005f408d6a5a5a44026c6d0b6b` | | `references/sandstone-cottage/generate.py` | `runs/exp8/sandstone-cottage/work/generator/workspace/generate.py` | `b895584fa4db7eaa5861c78f87fc666b72c350c4617b79587f5fd9d8ba89b1c2` | | `reference_sandstone_cottage.jpg` | `runs/exp8/sandstone-cottage/work/generator/workspace/reference_sandstone_cottage.jpg` | `91f219ccfaabab92f430db6847f9737930cbbc7adc7c29dec560a793e53ec409` | | `references/stilt-water-cabin/generate.py` | `runs/exp8/stilt-water-cabin/work/generator/workspace/generate.py` | `a8d11827e71a5765904af86cf47ebeef399071d53bee0ef32338828463ae5036` | | `reference_stilt_water_cabin.jpg` | `runs/exp8/stilt-water-cabin/work/generator/workspace/reference_stilt_water_cabin.jpg` | `f5f82de38845f10667852b8967a5854ba8574533a16a4cfaa20b699df47e8aee` | | `references/stilted-manor/generate.py` | `runs/exp8/stilted-manor/work/generator/workspace/generate.py` | `affdb6cba881a7b280a81833ba3fbd906e5730dea82d0cc37a4b1ae364868d3e` | | `reference_stilted_manor.jpg` | `runs/exp8/stilted-manor/work/generator/workspace/reference_stilted_manor.jpg` | `8b435bffc278bbbb64e79e4c4f2ed9188124a4c73557f6667fce46b266d29923` | | `references/stone-cottage/generate.py` | `runs/exp8/stone-cottage/work/generator/workspace/generate.py` | `da24c75f03bdd7a17f9201f31f24877301881beeee0d66cead309b269dd7f412` | | `reference_stone_cottage.jpg` | `runs/exp8/stone-cottage/work/generator/workspace/reference_stone_cottage.jpg` | `80116881f0af3f7d1a5d3937ecfb14a9afb99b42507cd047a0fb2239bbe56a5a` |
#071 · 2026-09-25T16:05:16.064Z · exit=0 · tail -5 samples/batch.log && tail -3 samples/audit.log && cat generate.py && cat examples/courtyard_
tail -5 samples/batch.log && tail -3 samples/audit.log && cat generate.py && cat examples/courtyard_house.py

日志保留的输出末尾

eeds # Harness client: declare, generate, validate and export one furnished property. from hearthwright import Attachment, Choice, Design, Level, Materials, Roof, Site, Volume from hearthwright import blockstates_equivalent from hearthwright.cli import run DESIGN = Design( 'Highbeam manor', volumes=(Volume('manor', width=Choice(19, 21), depth=Choice(17, 19), levels=(Level(('workshop', 'storage'), inset=2, wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross')), Level(('sleeping', 'study'), layout=Choice('open', 'cross'), attic=True)), roof=Roof(ridge='x', pitch=Choice(1.0, 1.5), dormers=2, cupola=True)),), materials=Materials(wall='plaster', frame='spruce', roof='gray', trim='spruce', accent='red'), site=Site(planting='conifer', margin=5), attachments=(Attachment('balcony', 'manor', level=1, side='east', width=7, depth=3),), ) if __name__ == '__main__': run(DESIGN) # A new composition: three connected masses frame a sheltered entrance court. from hearthwright import Attachment, Choice, Design, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Three gables court', volumes=( Volume('hall', width=Choice(23, 25), depth=Choice(11, 13), levels=(Level(('living', 'kitchen', 'study')), Level(('sleeping', 'storage'), layout='cross')), roof=Roof(ridge='x', pitch=.5)), Volume('work', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'front'), levels=(Level(('workshop', 'storage')),), roof=Roof(ridge='z', pitch=1.0)), Volume('guest', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'back'), levels=(Level(('sleeping', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='plaster', frame='spruce', roof='slate', accent='blue'), site=Site(margin=3, planting='sparse', paving='yard'), attachments=(Attachment('porch', 'hall', width=5, depth=3, cover='pergola'),), ) if __name__ == '__main__': run(DESIGN)
#072 · 2026-09-25T16:05:26.386Z · exit=0 · wc -l hearthwright/*.py test/test_*.py && cat pyproject.toml
wc -l hearthwright/*.py test/test_*.py && cat pyproject.toml

日志保留的输出末尾

10 hearthwright/__init__.py 196 hearthwright/api.py 250 hearthwright/blocks.py 400 hearthwright/build.py 18 hearthwright/cli.py 37 hearthwright/components.py 169 hearthwright/furnish.py 30 hearthwright/materials.py 68 hearthwright/navigation.py 385 hearthwright/plan.py 215 hearthwright/roofs.py 401 hearthwright/site.py 306 hearthwright/validate.py 101 test/test_api.py 77 test/test_corpus.py 88 test/test_roundtrip.py 113 test/test_validation.py 2864 total [build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" [project] name = "hearthwright" version = "0.1.0" description = "Composable Minecraft architecture with geometry validation" requires-python = ">=3.11" [tool.setuptools.packages.find] include = ["hearthwright*"] [tool.pytest.ini_options] pythonpath = ["."] testpaths = ["test"]
#073 · 2026-09-25T16:05:30.270Z · exit=0 · xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litem
xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews/default > previews/default-render.log 2>&1

日志保留的输出末尾

#074 · 2026-09-25T16:05:36.475Z · exit=0 · sed -n '1,240p' hearthwright/api.py && cat test/test_validation.py && cat test/test_corpus.py
sed -n '1,240p' hearthwright/api.py && cat test/test_validation.py && cat test/test_corpus.py

日志保留的输出末尾

data['structural_signature'], b.metadata['structural_signature']]) geometries.update([a.metadata['geometry_signature'], b.metadata['geometry_signature']]) assert len(signatures) == len(CLIENTS) * 2 assert len(geometries) == len(CLIENTS) * 2 def test_water_posts_are_not_manor_overhangs(): from examples.stilt_water_cabin import DESIGN as WATER from examples.stilted_manor import DESIGN as MANOR wet = generate(WATER, 0) dry = generate(MANOR, 0) assert wet.plan.masses[0].supports == 'posts' assert wet.plan.masses[0].levels[0].floor > WATER.site.water_depth assert dry.plan.masses[0].levels[0].rect.width < dry.plan.masses[0].levels[1].rect.width assert dry.plan.design.site.terrain != 'water' assert any(s['owner'] == 'manor/1' for s in dry.metadata['supports']) def test_root_envelope_is_nonrectangular_and_buried_floor_excavated(): from examples.root_house import DESIGN as ROOT from examples.sandstone_cottage import DESIGN as SLOPE root = generate(ROOT, 0) level = root.plan.masses[0].levels[0] assert len(level.cells) < level.rect.width * level.rect.depth assert any('dark_oak_log' in s and (x, z) not in level.cells for (x, y, z), s in root.grid.blocks.items()) slope = generate(SLOPE, 0) assert slope.plan.design.site.terrain == 'slope' assert any('water' in state for state in slope.grid.blocks.values()) assert slope.validate().valid def test_structural_signatures_exclude_material_noise_and_furniture(): a = Design('A', (Volume('home', width=13, depth=13),), materials=Materials(wall='stone', roof='slate')) b = replace(a, name='B', materials=Materials(wall='plaster', roof='red', accent='blue')) first, second = generate(a, 11), generate(b, 11) assert first.grid.blocks != second.grid.blocks assert first.metadata['structural_signature'] == second.metadata['structural_signature'] assert first.metadata['geometry_signature'] == second.metadata['geometry_signature']
#075 · 2026-09-25T16:05:36.516Z · exit=0 · tail -4 samples/audit.log && tail -4 previews/render.log
tail -4 samples/audit.log && tail -4 previews/render.log

日志保留的输出末尾

mediterranean_villa: audited 64 seeds merchant_manor: audited 64 seeds
#076 · 2026-09-25T16:05:48.137Z · exit=0 · /opt/venv/bin/yapf --style='{based_on_style: google, column_limit: 100}' -i generate.py examples/*.p
/opt/venv/bin/yapf --style='{based_on_style: google, column_limit: 100}' -i generate.py examples/*.py

日志保留的输出末尾

#077 · 2026-09-25T16:05:48.178Z · exit=0 · cat generate.py && cat examples/mediterranean_villa.py
cat generate.py && cat examples/mediterranean_villa.py

日志保留的输出末尾

, levels=(Level(('workshop', 'storage'), inset=2, wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross')), Level(('sleeping', 'study'), layout=Choice('open', 'cross'), attic=True)), roof=Roof(ridge='x', pitch=Choice(1.0, 1.5), dormers=2, cupola=True)),), materials=Materials(wall='plaster', frame='spruce', roof='gray', trim='spruce', accent='red'), site=Site(planting='conifer', margin=5), attachments=(Attachment('balcony', 'manor', level=1, side='east', width=7, depth=3),), ) if __name__ == '__main__': run(DESIGN) # Offset plaster volumes, turquoise shutters, open terraces and a garden pool. from hearthwright import Attachment, Choice, Design, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'House of the blue courtyard', volumes=( Volume('house', width=Choice(15, 17), depth=Choice(13, 15), levels=(Level(('living', 'kitchen')), Level(('sleeping', 'brewing'), layout=Choice('open', 'cross'))), roof=Roof(ridge='x', pitch=.5)), Volume('library', width=Choice(9, 11), depth=9, join=Join('house', 'east', Choice('front', 'back')), levels=(Level(('enchanting', 'study')),), roof=Roof(pitch=.5, ridge='z')), ), materials=Materials(wall='white', frame='birch', roof='tile', trim='sandstone', accent='cyan'), site=Site(planting='mediterranean', paving='courtyard', margin=3), attachments=(Attachment('terrace', 'house', depth=4, cover='pergola'), Attachment('balcony', 'house', level=1, side='west', width=7, cover='pergola')), features=(Feature('pool', 'south', Choice(7, 9)), Feature('garden', 'west', 5)), ) if __name__ == '__main__': run(DESIGN)
#078 · 2026-09-25T16:05:53.148Z · exit=0 · tail -6 samples/audit.log && tail -6 previews/render.log && cat previews/default-render.log
tail -6 samples/audit.log && tail -6 previews/render.log && cat previews/default-render.log

日志保留的输出末尾

ork/generator/workspace/previews/woodland_archive-1/front.png (768x768, azimuth 35, elevation 26) parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! parent minecraft:builtin/entity does not exist! Using WebGL2 context for WebGL requests instead of WebGL Loaded structure from /work/generator/workspace/output.litematic in 17 ms Set structure in renderer in 353 ms Rendering output at azimuth 0 (1/1)... Rendered image in 30 ms (azimuth: 3.6651914291880923, elevation: 0.3141592653589793) wrote /work/generator/workspace/previews/default/front.png (512x512, azimuth 210, elevation 18) Loaded structure from /work/generator/workspace/output.litematic in 18 ms Set structure in renderer in 130 ms Rendering output at azimuth 0 (1/1)... Rendered image in 139 ms (azimuth: 0.6108652381980153, elevation: 0.3839724354387525) wrote /work/generator/workspace/previews/default/rear.png (512x512, azimuth 35, elevation 22) Loaded structure from /work/generator/workspace/output.litematic in 15 ms Set structure in renderer in 261 ms Rendering output at azimuth 0 (1/1)... Rendered image in 15 ms (azimuth: 2.0943951023931953, elevation: 0.24434609527920614) wrote /work/generator/workspace/previews/default/side.png (512x512, azimuth 120, elevation 14) Loaded structure from /work/generator/workspace/output.litematic in 17 ms Set structure in renderer in 262 ms Rendering output at azimuth 0 (1/1)... Rendered image in 18 ms (azimuth: 3.6651914291880923, elevation: 1.0821041362364843) wrote /work/generator/workspace/previews/default/top.png (512x512, azimuth 210, elevation 62)
#079 · 2026-09-25T16:05:56.440Z · exit=0 · /opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1
/opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1

日志保留的输出末尾

#080 · 2026-09-25T16:06:34.664Z · exit=0 · tail -8 samples/audit.log && tail -3 previews/render.log
tail -8 samples/audit.log && tail -3 previews/render.log

日志保留的输出末尾

red_roof_farmstead: audited 64 seeds root_house: audited 64 seeds sandstone_cottage: audited 64 seeds stilt_water_cabin: audited 64 seeds stilted_manor: audited 64 seeds stone_cottage: audited 64 seeds canal_apothecary: audited 64 seeds woodland_archive: audited 64 seeds Rendering floor-1 at azimuth 0 (1/1)... Rendered image in 8 ms (azimuth: 0.6108652381980153, elevation: 1.1344640137963142) wrote /work/generator/workspace/previews/courtyard_house-1/floor-1.png (768x768, azimuth 35, elevation 65)
#081 · 2026-09-25T16:06:44.655Z · exit=0 · cat hearthwright/furnish.py
cat hearthwright/furnish.py

日志保留的输出末尾

n.materials for room in ctx.rooms: # Fit long and multi-block pieces first, before optional decor. groups = [(kind, pieces, function) for function in room['functions'] for kind, pieces in modules(function, mat, len(room['cells']))] groups.sort(key=lambda item: -len({(p[0], p[2]) for p in item[1]})) for kind, pieces, function in groups: place_group(ctx, room, kind, pieces, function) # The lamp module carries its own real support, including in irregular attics. place_group(ctx, room, 'lamp', [(0, 0, 0, mat.floor + '_planks', {}), (0, 1, 0, 'lantern', {'hanging': 'false'})], 'lighting') lamp = g.metadata['furniture'][-1]['cells'][-1] g.metadata['lights'].append({'owner': room['id'], 'point': lamp}) if len(room['cells']) > 100 and room['height'] >= 5: # Two supported pendant lamps illuminate larger domestic rooms. ax, _, az = room['anchor'] candidates = sorted(room['cells'], key=lambda p: abs(p[0] - ax) + abs(p[1] - az)) pendants = [] for x, z in candidates: y = room['floor'] + room['height'] - 1 if any(abs(x - px) + abs(z - pz) < 6 for px, pz in pendants): continue if full_support(g.get(x, y + 1, z)) and g.free([(x, y, z)]): g.lantern(x, y, z, room['id'], True) pendants.append((x, z)) if len(pendants) == 2: break # Inlaid rugs add domestic color without reducing headroom or obstructing steps. ax, _, az = room['anchor'] for x in range(ax - 2, ax + 3): for z in range(az - 1, az + 2): if (x, z) in room['cells'] and full_support(g.get(x, room['floor'], z)) and is_air(g.get(x, room['floor'] + 1, z)): g.put(x, room['floor'], z, (mat.accent if abs(x - ax) == 2 or abs(z - az) == 1 else 'white') + '_terracotta')
#082 · 2026-09-25T16:06:44.704Z · exit=0 · cat hearthwright/navigation.py && sed -n '265,390p' hearthwright/build.py
cat hearthwright/navigation.py && sed -n '265,390p' hearthwright/build.py

日志保留的输出末尾

x, z) or y < bottom: continue n = name(state) role = None if n.endswith('_door') or n.endswith('_stairs') or n == 'ladder': properties = split_state(state)[1] role = ('door' if n.endswith('_door') else 'stair' if n.endswith('_stairs') else 'ladder') + ':' + properties.get('facing', '') + ':' + properties.get('half', '') elif 'glass' in n: role = 'glass' elif n.endswith('_slab'): role = 'slab:' + split_state(state)[1].get('type', '') elif full_support(state): role = 'solid' if role: selected.append((x, y, z, role)) return hashlib.sha256(json.dumps(selected, separators=(',', ':')).encode()).hexdigest() def check_limits(self): b = self.g.bounds size = [b[i + 3] - b[i] + 3 for i in range(3)] limits = self.plan.design.limits if max(size[0], size[2]) > limits.horizontal or size[1] > limits.vertical or size[0] * size[1] * size[2] > limits.volume or len(self.g.blocks) > limits.blocks: raise DesignError(f'final geometry size {size} / {len(self.g.blocks)} blocks exceeds Limits; increase resource limits or reduce the composition') class Building: """Expose a generated artifact, design metadata, validation and deterministic export.""" def __init__(self, grid, plan=None): self.grid = grid self.plan = plan @property def metadata(self): return self.grid.metadata def validate(self): from .validate import validate return validate(self) def export(self, path: Path): self.validate().require_valid() return self.grid.export(Path(path), self.metadata['name']) def verify(self, path: Path): return self.grid.verify(Path(path)) @classmethod def load(cls, path: Path, metadata=None):
#083 · 2026-09-25T16:06:59.119Z · exit=0 · /opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1
/opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1

日志保留的输出末尾

#084 · 2026-09-25T16:07:30.552Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from pathlib import Path import json, re, hashlib m=json.loads(Path('samples/manifest.json').read_text()) a=json.loads(Path('samples/audit.json').read_text()) print('Samples:',len(m['samples']),'observed structures:',m['observed_structures'],'observed geometry signatures:',m['observed_geometry_signatures']) print('Audit cases:',len(a['seeds'])*len(a['clients']),'failures:',len(a['failures'])) print(json.dumps(a['clients'],indent=2)) checks=re.findall(r'\| `([^`]+)` \| `[^`]+` \| `([a-f0-9]{64})` \|',Path('references/README.md').read_text()) assert len(checks)==16 for path,expected in checks: assert hashlib.sha256(Path(path).read_bytes()).hexdigest()==expected,path print('Reference provenance: all 16 supplied hashes unchanged') print('Previews:',len(list(Path('previews').rglob('*.png')))) PY

日志保留的输出末尾

Samples: 26 observed structures: 26 observed geometry signatures: 26 Audit cases: 832 failures: 0 { "canal_apothecary": { "observed_geometry_signatures": 8, "observed_structures": 8 }, "courtyard_house": { "observed_geometry_signatures": 16, "observed_structures": 16 }, "hillside_studio": { "observed_geometry_signatures": 31, "observed_structures": 31 }, "mediterranean_villa": { "observed_geometry_signatures": 31, "observed_structures": 45 }, "merchant_manor": { "observed_geometry_signatures": 45, "observed_structures": 45 }, "orchard_house": { "observed_geometry_signatures": 24, "observed_structures": 45 }, "red_roof_farmstead": { "observed_geometry_signatures": 16, "observed_structures": 31 }, "root_house": { "observed_geometry_signatures": 38, "observed_structures": 38 }, "sandstone_cottage": { "observed_geometry_signatures": 31, "observed_structures": 31 }, "stilt_water_cabin": { "observed_geometry_signatures": 8, "observed_structures": 16 }, "stilted_manor": { "observed_geometry_signatures": 31, "observed_structures": 31 }, "stone_cottage": { "observed_geometry_signatures": 8, "observed_structures": 8 }, "woodland_archive": { "observed_geometry_signatures": 31, "observed_structures": 45 } } Reference provenance: all 16 supplied hashes unchanged Previews: 105
#085 · 2026-09-25T16:07:50.445Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' import json from pathlib import Path for client in ('merchant_manor','woodland_archive','mediterranean_villa'): m=json.loads(Path(f'samples/{client}-0.json').read_text()) print(client, m.get('origin'),[(r['id'],r['floor'],r['height']) for r in m['rooms']]) PY sed -n '1,150p' /opt/tools/cutaway.py

日志保留的输出末尾

if state.split("[")[0] in AIR: continue target = {"y": y, "z": z, "x": x} target[axis] = index - lower canvas.block((target["y"], target["z"], target["x"]), parse_blockstate(state)) schematic = canvas.to_litematica(name=name, author="cutaway", description=f"{source.name} {axis}<={cut}", minecraft_data_version=source_data_version(source)) # Deterministic output: the schematic metadata otherwise records the current wall clock. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 schematic.save(destination) def main(argv: Optional[list] = None) -> None: """Run one slice export. Args: argv (Optional[list]): Command line arguments, defaulting to sys.argv. """ parser = argparse.ArgumentParser(description="Slice a schematic so its interior can be rendered.") parser.add_argument("--input", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--axis", choices=("x", "y", "z"), default="y") parser.add_argument("--max", dest="limit", type=int, default=None, help="Last index to keep along the axis.") parser.add_argument("--max-y", dest="max_y", type=int, default=None, help="Shorthand for --axis y --max N.") parser.add_argument("--keep-above", action="store_true", help="Keep the far side of the cut instead.") parser.add_argument("--name", default="slice") args = parser.parse_args(argv) axis = "y" if args.max_y is not None else args.axis limit = args.max_y if args.max_y is not None else args.limit if limit is None: parser.error("give --max N (with --axis) or --max-y N") slice_schematic(args.input, args.output, axis, limit, args.keep_above, args.name) side = "above" if args.keep_above else "up to" print(f"wrote {args.output} ({side} {axis}={limit})") if __name__ == "__main__": main()
#086 · 2026-09-25T16:08:32.461Z · exit=0 · tail -3 previews/render.log && cat hearthwright/cli.py && cat hearthwright/__init__.py && cat test/t
tail -3 previews/render.log && cat hearthwright/cli.py && cat hearthwright/__init__.py && cat test/test_api.py

日志保留的输出末尾

ots'),)), 'roof=None'), (Design('tiny', (Volume('home', width=7),)), 'width'), (Design('duplicate', (Volume('home'), Volume('home'))), 'duplicate'), (Design('oversized', (Volume('home'),), limits=Limits(horizontal=15)), 'resource limit'), (Design('unsupported material', (Volume('home'),), materials=Materials(roof='wool')), 'roof role'), (Design('missing join', (Volume('a'), Volume('b'))), 'Join'), ]) def test_reject_contradictory_intent(design, match): with pytest.raises(DesignError, match=match): generate(design, 0) @pytest.mark.parametrize('seed', [-(2**63) - 1, 2**63, 1.5, '1', True]) def test_seed_domain(seed): with pytest.raises(DesignError, match='signed 64-bit'): resolve(Design('seed', (Volume('home'),)), seed) def test_invalid_domains(): with pytest.raises(ValueError): Span(10, 3) with pytest.raises(ValueError): Span(3, 10, 0) with pytest.raises(ValueError): Choice() def test_registered_component_uses_shared_path_and_validation(): from hearthwright.components import SITE_COMPONENTS calls = [] def realize(ctx, feature): r = feature['rect'] x, z = r.center y = ctx.ground[x, z] ctx.g.put(x, y + 1, z, 'barrel') ctx.g.lantern(x, y + 2, z, feature['id']) feature['target'] = (x, y + 1, r.z1 + 1) ctx.g.metadata['features'].append({'owner': feature['id'], 'kind': feature['kind'], 'target': feature['target']}) def rule(grid, metadata, report): calls.append(len(metadata['features'])) component = SiteComponent('tool_cache', realize, rule=rule, version='test-1') try: register_site_component(component) building = generate(Design('Extension', (Volume('home'),), features=(Feature('tool_cache'),)), 0) assert building.validate().valid assert calls assert building.plan.features[0]['version'] == 'test-1' finally: SITE_COMPONENTS.pop('tool_cache', None)
#087 · 2026-09-25T16:08:43.953Z · exit=0 · cat hearthwright/plan.py
cat hearthwright/plan.py

日志保留的输出末尾

e decisions so later constraints can backtrack within declared domains.""" def __init__(self, seed, tape): super().__init__(seed) self.tape = tape self.visited = [] def pick(self, candidates): if len(candidates) == 1: return candidates[0] first = self.randrange(len(candidates)) index = self.tape[len(self.visited)] if len(self.visited) < len(self.tape) else 0 index = min(index, len(candidates) - 1) self.visited.append((index, len(candidates))) return candidates[(first + index) % len(candidates)] def resolve(design: Design, seed: int, *, max_candidates=128) -> Plan: """Search bounded declared choices, retaining explicit decisions without fallback presets. Args: design: Architectural composition, with concrete values or bounded domains. seed: Signed 64-bit integer controlling all choice ordering. max_candidates: Maximum complete or partial plans examined before a diagnostic. Returns: Plan: Feasible resolved architecture with no geometric mutations to user intent. """ if type(seed) is not int or not -(2**63) <= seed < 2**63: raise DesignError('seed must be a signed 64-bit integer') tape = [] error = None for attempt in range(max_candidates): rng = _DecisionRandom(seed, tape) try: plan = _resolve(design, seed, rng) return plan except DesignError as exc: error = exc visited = rng.visited for i in range(len(visited) - 1, -1, -1): branch, count = visited[i] if branch + 1 < count: tape = [v[0] for v in visited[:i]] + [branch + 1] break else: raise DesignError(f'contradictory specification: {exc}') from exc raise DesignError(f'planning budget ({max_candidates} candidates) exhausted: {error}; narrow the conflicting domains')
#088 · 2026-09-25T16:08:56.035Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('GENERATOR.md') s=p.read_text().replace('each main-volume occupied floor as a cutaway','each occupied floor elevation as a cutaway').replace('cutaways at the main volume\'s occupied floors','cutaways at every occupied floor elevation, including taller joined wings') p.write_text(s) for name in ('side.png','top.png'): (Path('previews/stilted_manor-0')/name).unlink(missing_ok=True) PY /opt/venv/bin/yapf -i scripts/render_batch.py /opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1

日志保留的输出末尾

#089 · 2026-09-25T16:09:17.152Z · exit=0 · /opt/venv/bin/yapf -i hearthwright/plan.py test/test_api.py
/opt/venv/bin/yapf -i hearthwright/plan.py test/test_api.py /opt/venv/bin/python -m pytest -q test/test_api.py

日志保留的输出末尾

.................... [100%] 20 passed in 1.52s
#090 · 2026-09-25T16:09:34.544Z · exit=0 · tail -3 previews/render.log
tail -3 previews/render.log

日志保留的输出末尾

Rendering floor-1 at azimuth 0 (1/1)... Rendered image in 9 ms (azimuth: 0.6108652381980153, elevation: 1.1344640137963142) wrote /work/generator/workspace/previews/courtyard_house-1/floor-1.png (768x768, azimuth 35, elevation 65)
#091 · 2026-09-25T16:11:22.712Z · exit=0 · tail -8 test-results.txt
tail -8 test-results.txt

日志保留的输出末尾

........................................................................ [ 33%] ........................................................................ [ 67%] ............................................................
#092 · 2026-09-25T16:11:23.340Z · exit=0 · /opt/venv/bin/yapf -i hearthwright/furnish.py
/opt/venv/bin/yapf -i hearthwright/furnish.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#093 · 2026-09-25T16:12:06.085Z · exit=0 · /opt/venv/bin/python generate.py
/opt/venv/bin/python generate.py

日志保留的输出末尾

wrote output.litematic: {'blocks': 6493, 'states': 84, 'size_xyz': [38, 35, 36]}; validation passed
#094 · 2026-09-25T16:12:13.318Z · exit=0 · /opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1
/opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1

日志保留的输出末尾

#095 · 2026-09-25T16:12:30.259Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' import setuptools print('setuptools',setuptools.__version__) PY cat test/test_roundtrip.py cat test/conftest.py

日志保留的输出末尾

gn = Design('Roundtrip damage', (Volume('home'),)) b = generate(design, 0) path = artifact_dir / 'build.litematic' b.export(path) loaded = Building.load(path) p = b.metadata['rooms'][0]['anchor'] loaded.grid.put(*p, 'stone') loaded.grid.export(path) with pytest.raises(AssertionError, match='coordinate set|changed on reload'): b.verify(path) def test_inventory_corruption_is_observed_in_nbt(artifact_dir): b = generate(Design('Stocked home', (Volume('home'),)), 0) path = artifact_dir / 'stock.litematic' b.export(path) nbt = NBTFile.load_regardless_of_gzipped(path) region = next(iter(nbt['Regions'].values())) for tile in region['TileEntities']: if 'Items' in tile and len(tile['Items']): del tile['Items'][:] break NBTFile(nbt).save(path) assert 'inventory.changed' in {d.rule for d in validate(path).diagnostics} def test_harness_client_runs_from_another_directory(artifact_dir): import subprocess import sys root = Path(__file__).resolve().parents[1] subprocess.run([sys.executable, str(root / 'generate.py'), '--seed', '73'], cwd=artifact_dir, check=True, capture_output=True) default_path = artifact_dir / 'output.litematic' assert default_path.exists() assert validate(default_path).valid second = artifact_dir / 'explicit.litematic' subprocess.run([sys.executable, str(root / 'generate.py'), '--seed', '73', '--output', str(second)], cwd=artifact_dir, check=True, capture_output=True) assert default_path.read_bytes() == second.read_bytes() # Keep all transient test artifacts under test/ and remove them after each test. from pathlib import Path import tempfile import pytest @pytest.fixture def artifact_dir(): root = Path(__file__).parent / '.tmp' root.mkdir(exist_ok=True) with tempfile.TemporaryDirectory(dir=root) as directory: yield Path(directory) try: root.rmdir() except OSError: pass
#096 · 2026-09-25T16:12:31.754Z · exit=0 · tail -4 test-results.txt
tail -4 test-results.txt /opt/venv/bin/yapf -i hearthwright/furnish.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#097 · 2026-09-25T16:12:53.127Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('GENERATOR.md') s=p.read_text().replace('Alternatively, install the local Python package with `uv pip install --python /opt/venv/bin/python --no-build-isolation -e /work/generator/workspace`. MCIO and the block registry remain external runtime dependencies.', 'A standard `pyproject.toml` is also supplied for environments with setuptools >= 68. The supplied container does not include that build dependency; use the tested `PYTHONPATH` route here. MCIO and the block registry remain external runtime dependencies.') s=s.replace('Large rooms receive dining groups, wardrobes, longer work counters, shelving and additional lamps;', 'Large rooms receive dining groups, wardrobes, longer work counters, shelving and additional lamps. Rooms above 150 usable floor cells also receive function-specific reading tables, dressing furniture, pantry/island groups or parts racks;') s=s.replace('`program.missing`, `furnishing.changed`, `lighting.room`', '`program.missing`, `furnishing.changed`, `lighting.room`, `.approach`, `.feature`') s=s.replace('and every room has an actual near-floor light.', 'and rooms, entrances, approaches and working plots retain actual nearby lights.') p.write_text(s) PY tail -6 samples/batch.log tail -3 test-results.txt

日志保留的输出末尾

orchard_house seed=0: 4866 blocks, validated after reload orchard_house seed=1: 4780 blocks, validated after reload hillside_studio seed=0: 5264 blocks, validated after reload hillside_studio seed=1: 4776 blocks, validated after reload courtyard_house seed=0: 6683 blocks, validated after reload courtyard_house seed=1: 5854 blocks, validated after reload ........................................................................ [ 67%] .................................................................... [100%] 212 passed in 41.27s
#098 · 2026-09-25T16:13:02.058Z · exit=0 · tail -6 samples/audit.log
tail -6 samples/audit.log

日志保留的输出末尾

mediterranean_villa: audited 64 seeds merchant_manor: audited 64 seeds red_roof_farmstead: audited 64 seeds root_house: audited 64 seeds
#099 · 2026-09-25T16:13:05.779Z · exit=0 · xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litem
xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews/default > previews/default-render.log 2>&1

日志保留的输出末尾

#100 · 2026-09-25T16:13:26.198Z · exit=0 · tail -7 samples/audit.log && tail -4 previews/render.log && rg --files hearthwright examples scripts
tail -7 samples/audit.log && tail -4 previews/render.log && rg --files hearthwright examples scripts test | wc -l

日志保留的输出末尾

mediterranean_villa: audited 64 seeds merchant_manor: audited 64 seeds red_roof_farmstead: audited 64 seeds root_house: audited 64 seeds sandstone_cottage: audited 64 seeds stilt_water_cabin: audited 64 seeds Rendering stilt_water_cabin-0 at azimuth 0 (1/1)... Rendered image in 12 ms (azimuth: 3.6651914291880923, elevation: 0.41887902047863906) wrote /work/generator/workspace/previews/stilt_water_cabin-0/rear.png (768x768, azimuth 210, elevation 24) Loaded structure from /work/generator/workspace/previews/stilt_water_cabin-0/floor-0.litematic in 2 ms 72
#101 · 2026-09-25T16:13:34.056Z · exit=0 · /opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1
/opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1

日志保留的输出末尾

#102 · 2026-09-25T16:13:55.088Z · exit=0 · rg -n 'random|references|exp8|preset' hearthwright generate.py examples scripts && rg --files hearth
rg -n 'random|references|exp8|preset' hearthwright generate.py examples scripts && rg --files hearthwright examples scripts test | head -30 && du -sh hearthwright examples samples previews

日志保留的输出末尾

hearthwright/components.py:8: """Register a reusable geometry algorithm, not a complete building preset. hearthwright/furnish.py:131: # Favor corners and walls, while retaining randomized tie-breaks. hearthwright/site.py:365: if name(g.get(x, y - 1, z)) == 'grass_block' and g.rng.random() < .12: hearthwright/site.py:367: elif p.design.site.terrain == 'water' and name(g.get(x, p.design.site.water_depth, z)) == 'water' and g.rng.random() < .035: hearthwright/blocks.py:8:import random hearthwright/blocks.py:102: self.rng = random.Random(seed) hearthwright/plan.py:7:import random hearthwright/plan.py:337:class _DecisionRandom(random.Random): hearthwright/plan.py:356: """Search bounded declared choices, retaining explicit decisions without fallback presets. test/__pycache__/test_validation.cpython-312-pytest-9.1.1.pyc test/__pycache__/test_roundtrip.cpython-312-pytest-9.1.1.pyc test/__pycache__/test_corpus.cpython-312-pytest-9.1.1.pyc test/__pycache__/test_api.cpython-312-pytest-9.1.1.pyc test/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc test/test_validation.py test/test_roundtrip.py test/test_corpus.py test/test_api.py test/conftest.py examples/courtyard_house.py examples/stilt_water_cabin.py examples/hillside_studio.py examples/sandstone_cottage.py examples/orchard_house.py examples/root_house.py examples/red_roof_farmstead.py examples/merchant_manor.py examples/mediterranean_villa.py examples/stone_cottage.py examples/__init__.py examples/canal_apothecary.py examples/stilted_manor.py examples/woodland_archive.py scripts/audit.py examples/__pycache__/courtyard_house.cpython-312.pyc examples/__pycache__/hillside_studio.cpython-312.pyc examples/__pycache__/orchard_house.cpython-312.pyc examples/__pycache__/woodland_archive.cpython-312.pyc examples/__pycache__/canal_apothecary.cpython-312.pyc 400K hearthwright 120K examples 6.5M samples 67M previews
#103 · 2026-09-25T16:14:14.527Z · exit=0 · /opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1
/opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1

日志保留的输出末尾

#104 · 2026-09-25T16:14:33.222Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from hearthwright import * cases = { 'four-storey': Design('Four floors',(Volume('home',width=15,depth=17,levels=(Level(('workshop','storage'),height=7),Level(('living','kitchen'),height=6),Level(('sleeping','study')),Level(('sleeping','storage'),attic=True)),roof=Roof(ridge='z',pitch=1.5,eaves=2)),)), 'deep-water': Design('Deep shallows',(Volume('home',width=13,depth=11,raised=5,supports='posts',roof=Roof(form='hip',pitch=.5,eaves=2)),),site=Site(terrain='water',water_depth=3),attachments=(Attachment('deck','home',side='west',depth=7),),entrance=Entrance('home','west')), 'hip-attic': Design('Hip loft',(Volume('home',width=15,depth=15,levels=(Level(('living','kitchen')),Level(('sleeping','study'),attic=True)),roof=Roof(form='hip',pitch=1,eaves=2)),)), } for side in ('north','south','east','west'): for terrain in ('meadow','slope'): cases[side+'-'+terrain]=Design('Side entry',(Volume('home',width=13,depth=13),),site=Site(terrain=terrain,rise=7),entrance=Entrance('home',side)) for key,design in cases.items(): try: b=generate(design,17) print(key,'VALID',len(b.grid.blocks)) except Exception as e: print(key,'FAILED',str(e)[:1800]) PY

日志保留的输出末尾

four-storey VALID 6353 deep-water VALID 5438 hip-attic VALID 4239 north-meadow VALID 3217 north-slope FAILED no clear site edge for an approach lamp south-meadow VALID 3217 south-slope VALID 5086 east-meadow VALID 3219 east-slope VALID 5527 west-meadow VALID 3217 west-slope VALID 5529
#105 · 2026-09-25T16:14:42.786Z · exit=0 · tail -6 samples/audit.log && tail -3 previews/render.log
tail -6 samples/audit.log && tail -3 previews/render.log

日志保留的输出末尾

stone_cottage: audited 64 seeds canal_apothecary: audited 64 seeds woodland_archive: audited 64 seeds orchard_house: audited 64 seeds hillside_studio: audited 64 seeds courtyard_house: audited 64 seeds Rendering floor-1 at azimuth 0 (1/1)... Rendered image in 16 ms (azimuth: 0.6108652381980153, elevation: 1.1344640137963142) wrote /work/generator/workspace/previews/courtyard_house-1/floor-1.png (768x768, azimuth 35, elevation 65)
#106 · 2026-09-25T16:14:53.324Z · exit=0 · sed -n '1,205p' hearthwright/site.py && sed -n '220,330p' hearthwright/site.py
sed -n '1,205p' hearthwright/site.py && sed -n '220,330p' hearthwright/site.py

日志保留的输出末尾

eue.popleft() for dx, dz in DIRECTIONS.values(): q = x + dx, z + dz if q not in parents and q not in forbidden and p.site.contains(*q): parents[q] = (x, z) queue.append(q) if target2 not in parents: raise ValueError(f'{owner}: no clear exterior approach; change its side') path = [] q = target2 while q is not None: path.append(q) q = parents[q] path.reverse() # Paths use the entrance landing elevation. Terraced rises use a dedicated last run. start_y = ctx.start[1] - 1 target_y = target[1] - 1 difference = target_y - start_y if abs(difference) > len(path) - 2: raise ValueError(f'{owner}: path too short for terrain transition') points = [] for i, (x, z) in enumerate(path): h = start_y remaining = len(path) - 1 - i if remaining < abs(difference): h += (1 if difference > 0 else -1) * (abs(difference) - remaining) g.box((x, 0, z, x, h - 1, z), 'stone_bricks' if p.design.site.terrain != 'water' else 'spruce_planks') g.put(x, h, z, 'coarse_dirt' if p.design.site.terrain != 'water' else 'spruce_planks') g.box((x, h + 1, z, x, max(h + 3, ctx.ground.get((x, z), 1) + 2), z), 'air') if difference > 0 and remaining < difference: px, pz = path[i - 1] facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - px, z - pz)) g.stair(x, h, z, 'stone_brick', facing) elif difference < 0 and remaining <= -difference and i + 1 < len(path): nx, nz = path[i + 1] facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - nx, z - nz)) g.stair(x, h, z, 'stone_brick', facing) points.append((x, h + 1, z)) g.route(points, owner)
#107 · 2026-09-25T16:16:47.281Z · exit=0 · /opt/venv/bin/yapf -i hearthwright/plan.py hearthwright/site.py
/opt/venv/bin/yapf -i hearthwright/plan.py hearthwright/site.py /opt/venv/bin/python - <<'PY' from hearthwright import * from hearthwright.navigation import neighbors,walkable from hearthwright.materials import DIRECTIONS for side in ('north','south','east','west'): for terrain in ('meadow','slope'): d=Design('Side entry',(Volume('home',width=13,depth=13),),site=Site(terrain=terrain,rise=7),entrance=Entrance('home',side)) try: b=generate(d,17) x,y,z=b.metadata['start']; dx,dz=DIRECTIONS[side] assert walkable(b.grid,(x+dx,y,z+dz)) print(side,terrain,'VALID',b.metadata['start']) except Exception as e: print(side,terrain,'FAILED',str(e)[:1800]) PY

日志保留的输出末尾

north meadow VALID (6, 2, -5) north slope VALID (6, 9, -10) south meadow VALID (6, 2, 17) south slope VALID (6, 2, 17) east meadow VALID (17, 2, 6) east slope VALID (19, 6, 6) west meadow VALID (-5, 2, 6) west slope VALID (-7, 6, 6)
#108 · 2026-09-25T16:17:02.835Z · exit=0 · sed -n '1,95p' hearthwright/build.py && sed -n '105,190p' hearthwright/validate.py && sed -n '190,27
sed -n '1,95p' hearthwright/build.py && sed -n '105,190p' hearthwright/validate.py && sed -n '190,270p' hearthwright/validate.py

日志保留的输出末尾

report.add('reachability.furniture', f['owner'], f'{f["kind"]} interaction space is blocked', f['access']) report.checked['reachable_positions'] = len(reachable) report.checked['rooms'] = len(m['rooms']) def envelope(g, m, report): """Flood interior voids to detect leaks, including later damage to roofs and walls.""" interior = {tuple(p) for p in m['interior']} visited = set() for room in m['rooms']: start = tuple(room['anchor']) if start in visited or seal(g.get(*start)): continue queue = deque([start]) visited.add(start) failure = None while queue and failure is None: x, y, z = queue.popleft() for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)): q = x + dx, y + dy, z + dz if q in visited or seal(g.get(*q)): continue if q not in interior: failure = q break visited.add(q) queue.append(q) if failure is not None: report.add('envelope.leak', room['id'], 'interior air reaches outside its weather envelope', failure) for window in m['openings']: if window['kind'] != 'window': continue side = window['side'] dx, dz = DIRECTIONS[side] for x, y, z in window['cells']: if 'glass' not in name(g.get(x, y, z)): report.add('opening.glazing', window['owner'], 'glazed aperture was overwritten', (x, y, z)) break # A room-side full block obstructs light and view; roof cavities are exempt. q = x - dx, y, z - dz if q in interior and not passable(g.get(*q)): report.add('opening.clearance', window['owner'], 'window reveal is blocked inside the room', q) break for room in m['rooms']: for x, z in room['cells']:
#109 · 2026-09-25T16:18:22.047Z · exit=0 · tail -12 test-results.txt
tail -12 test-results.txt

日志保留的输出末尾

..............................................................F......... [ 32%] ...F.................................................................... [ 64%] ......................F............
#110 · 2026-09-25T16:18:33.803Z · exit=1 · /opt/venv/bin/yapf -i hearthwright/validate.py test/test_corpus.py test/test_validation.py
/opt/venv/bin/yapf -i hearthwright/validate.py test/test_corpus.py test/test_validation.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#111 · 2026-09-25T16:18:33.827Z · exit=0 · tail -95 test-results.txt
tail -95 test-results.txt

日志保留的输出末尾

achability.terrain: approach (6, 2, 20): outer landing does not meet clear, walkable terrain hearthwright/validate.py:39: ValidationError ____ test_reference_and_novel_compositions[-9223372036854775808-root_house] ____ client = 'root_house', seed = -9223372036854775808 @pytest.mark.parametrize('client', CLIENTS) @pytest.mark.parametrize('seed', SEEDS) def test_reference_and_novel_compositions(client, seed): design = importlib.import_module('examples.' + client).DESIGN > building = generate(design, seed) ^^^^^^^^^^^^^^^^^^^^^^ test/test_corpus.py:17: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ hearthwright/build.py:399: in generate artifact.validate().require_valid() _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = Report(diagnostics=[Diagnostic(rule='reachability.terrain', component='approach', message='outer landing does not meet...rt_columns': 0, 'reachable_positions': 208, 'rooms': 2, 'envelope_voids': 897, 'furniture_groups': 11, 'blocks': 7178}) def require_valid(self): if not self.valid: details = '\n'.join(f'{d.rule}: {d.component} {d.location or ""}: {d.message}' for d in self.diagnostics[:25]) > raise ValidationError(f'{len(self.diagnostics)} architectural validation failures\n{details}', self) E hearthwright.validate.ValidationError: 1 architectural validation failures E reachability.terrain: approach (7, 2, 18): outer landing does not meet clear, walkable terrain hearthwright/validate.py:39: ValidationError =========================== short test summary info ============================ FAILED test/test_corpus.py::test_reference_and_novel_compositions[3-root_house] FAILED test/test_corpus.py::test_reference_and_novel_compositions[4-root_house] FAILED test/test_corpus.py::test_reference_and_novel_compositions[-9223372036854775808-root_house] 3 failed, 221 passed in 42.26s
#112 · 2026-09-25T16:18:39.421Z · exit=0 · sed -n '155,250p' hearthwright/roofs.py && sed -n '335,420p' hearthwright/site.py
sed -n '155,250p' hearthwright/roofs.py && sed -n '335,420p' hearthwright/site.py

日志保留的输出末尾

om() < .12: g.put(x, y, z, g.rng.choice(('short_grass', 'short_grass', 'fern', 'cornflower', 'oxeye_daisy'))) elif p.design.site.terrain == 'water' and name(g.get(x, p.design.site.water_depth, z)) == 'water' and g.rng.random() < .035: g.put(x, p.design.site.water_depth + 1, z, 'lily_pad') def tree(ctx, x, z, policy): """Grow a small connected tree using the site's planting vocabulary.""" g = ctx.g base = ctx.ground[x, z] height = g.rng.randint(9, 12) if policy == 'conifer' else g.rng.randint(7, 10) if policy == 'mediterranean' else g.rng.randint(5, 8) wood = 'jungle' if policy == 'mediterranean' else 'spruce' if policy == 'conifer' else 'oak' g.box((x, base + 1, z, x, base + height, z), wood + '_log', axis='y') leaves = set() if policy == 'mediterranean': for dx, dz in DIRECTIONS.values(): for d in range(4): leaves.add((x + dx * d, base + height + (1 if d < 3 else 0), z + dz * d)) if d == 3: leaves.add((x + dx * d, base + height - 1, z + dz * d)) elif policy == 'conifer': for level in range(3, height + 2): radius = max(0, (height - level) // 4 + (1 if level % 3 == 0 else 0)) for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dz) <= radius + 1: leaves.add((x + dx, base + level, z + dz)) else: for dy in range(-3, 2): radius = max(0, 2 - abs(dy) // 2) if policy != 'conifer' else (2 if dy % 2 == 0 else 1) for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dz) <= radius + 1: leaves.add((x + dx, base + height + dy, z + dz)) for point in sorted(leaves): if is_air(g.get(*point)): g.put(*point, wood + '_leaves', persistent='true', distance='1')
#113 · 2026-09-25T16:19:33.686Z · exit=0 · /opt/venv/bin/python generate.py
/opt/venv/bin/python generate.py

日志保留的输出末尾

wrote output.litematic: {'blocks': 6493, 'states': 84, 'size_xyz': [38, 35, 36]}; validation passed
#114 · 2026-09-25T16:19:41.052Z · exit=0 · /opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1
/opt/venv/bin/python -m scripts.sample_batch > samples/batch.log 2>&1

日志保留的输出末尾

#115 · 2026-09-25T16:20:00.183Z · exit=0 · /opt/venv/bin/yapf -i hearthwright/plan.py
/opt/venv/bin/yapf -i hearthwright/plan.py /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#116 · 2026-09-25T16:20:32.747Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('GENERATOR.md') s=p.read_text().replace('A choice is uniform over currently feasible alternatives. Concrete values remain requirements.', 'Each choice starts with a seeded uniform draw over locally feasible alternatives. Bounded backtracking can change that selection; the library does not promise uniform sampling of complete designs. Concrete values remain requirements.') s=s.replace('The exterior approach uses the real entry height and connects through any matching terrace/deck.', 'The exterior approach uses the real entry and terrain heights, rises or descends as required, and connects through any matching terrace/deck. Root approaches continue beyond the full mound and buttresses.') s=s.replace('actual geometry to every room anchor,', 'actual geometry to every room anchor,') s=s.replace('Voxel traversal from the exterior start through the final geometry', 'A walkable transition from surrounding terrain to the outer landing, followed by voxel traversal through the final geometry') s=s.replace('plus targeted size/pitch and interaction cases.', 'plus targeted size/pitch and interaction cases: four storeys, hipped attics, maximum water depth, wide eaves, and entrances facing all four directions on flat and sloping sites.') p.write_text(s) PY tail -5 test-results.txt tail -3 samples/batch.log

日志保留的输出末尾

........................................................................ [ 32%] ........................................................................ [ 64%] ........................................................................ [ 96%] ........ [100%] 224 passed in 43.07s hillside_studio seed=1: 4934 blocks, validated after reload courtyard_house seed=0: 6683 blocks, validated after reload courtyard_house seed=1: 5854 blocks, validated after reload
#117 · 2026-09-25T16:20:46.566Z · exit=0 · tail -6 samples/audit.log
tail -6 samples/audit.log

日志保留的输出末尾

mediterranean_villa: audited 64 seeds merchant_manor: audited 64 seeds red_roof_farmstead: audited 64 seeds root_house: audited 64 seeds sandstone_cottage: audited 64 seeds stilt_water_cabin: audited 64 seeds
#118 · 2026-09-25T16:21:17.484Z · exit=0 · /opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1
/opt/venv/bin/python -m scripts.render_batch --all-seeds > previews/render.log 2>&1

日志保留的输出末尾

#119 · 2026-09-25T16:21:43.002Z · exit=0 · /opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1
/opt/venv/bin/python -m scripts.audit --count 64 > samples/audit.log 2>&1

日志保留的输出末尾

#120 · 2026-09-25T16:21:51.488Z · exit=0 · tail -6 samples/audit.log && tail -3 previews/render.log
tail -6 samples/audit.log && tail -3 previews/render.log

日志保留的输出末尾

stone_cottage: audited 64 seeds canal_apothecary: audited 64 seeds woodland_archive: audited 64 seeds orchard_house: audited 64 seeds hillside_studio: audited 64 seeds courtyard_house: audited 64 seeds Rendering floor-1 at azimuth 0 (1/1)... Rendered image in 8 ms (azimuth: 0.6108652381980153, elevation: 1.1344640137963142) wrote /work/generator/workspace/previews/courtyard_house-1/floor-1.png (768x768, azimuth 35, elevation 65)
#121 · 2026-09-25T16:22:26.720Z · exit=0 · /opt/venv/bin/yapf -i hearthwright/plan.py test/test_api.py
/opt/venv/bin/yapf -i hearthwright/plan.py test/test_api.py /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('GENERATOR.md') s=p.read_text().replace('A slope rises behind the south approach and is excavated out of all inhabited cells.', 'A slope rises toward the north and is excavated out of all inhabited cells. Hill sites require a root envelope, whose rooms and root parameters define the mound; use a slope for framed earth-sheltered buildings. The rise parameter belongs to slopes.') p.write_text(s) PY /opt/venv/bin/python -m pytest -q > test-results.txt 2>&1

日志保留的输出末尾

#122 · 2026-09-25T16:22:49.800Z · exit=0 · sed -n '20,45p' render.mjs && tail -4 test-results.txt && /opt/venv/bin/python - <<'PY'
sed -n '20,45p' render.mjs && tail -4 test-results.txt && /opt/venv/bin/python - <<'PY' from pathlib import Path import json m=json.loads(Path('samples/manifest.json').read_text()); a=json.loads(Path('samples/audit.json').read_text()) print('samples',len(m['samples']),'geometry signatures',m['observed_geometry_signatures']) print('audit',len(a['clients'])*len(a['seeds']),'failures',a['failures']) print('PNG files',sum(1 for _ in Path('previews').rglob('*.png'))) print('cutaway intermediates',sum(1 for _ in Path('previews').rglob('*.litematic'))) print('test temp remains',Path('test/.tmp').exists()) PY

日志保留的输出末尾

const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [47, 52, 59, 255]; const VIEWS = [ { name: 'front', azimuth: 210, elevation: 18, radius: 3.3 }, { name: 'rear', azimuth: 35, elevation: 22, radius: 3.3 }, { name: 'side', azimuth: 120, elevation: 14, radius: 3.3 }, { name: 'top', azimuth: 210, elevation: 62, radius: 3.4 }, ]; // Resolve every path while the working directory is still the caller's. const absoluteInput = path.resolve(INPUT); const absoluteOutDir = path.resolve(OUT_DIR); fs.mkdirSync(absoluteOutDir, { recursive: true }); // The renderer looks for its resource cache relative to the working directory, so move there first, // and only then create the scene. process.chdir(RENDER_ROOT); const scene = createRenderScene({ width: SIZE, height: SIZE }); for (const view of VIEWS) { const result = renderLitematicWithMetadata(absoluteInput, { width: SIZE, height: SIZE, fov_deg: 30, ........................................................................ [ 64%] ........................................................................ [ 96%] ......... [100%] 225 passed in 42.75s samples 26 geometry signatures 26 audit 832 failures [] PNG files 107 cutaway intermediates 0 test temp remains False
#123 · 2026-09-25T16:23:52.512Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from pathlib import Path import hashlib,json,re from hearthwright import Building from scripts.sample_batch import CLIENTS manifest=json.loads(Path('samples/manifest.json').read_text()) audit=json.loads(Path('samples/audit.json').read_text()) assert len(manifest['samples'])==26 and not audit['failures'] assert all(s['validation']['valid'] for s in manifest['samples']) Building.load(Path('output.litematic')).validate().require_valid() rows=[] for c in CLIENTS: obs=audit['clients'][c] rows.append(f'| `{c}` | 64 | {obs["observed_geometry_signatures"]} |') checks=re.findall(r'\| `([^`]+)` \| `[^`]+` \| `([a-f0-9]{64})` \|',Path('references/README.md').read_text()) assert len(checks)==16 assert all(hashlib.sha256(Path(p).read_bytes()).hexdigest()==digest for p,digest in checks) sha=hashlib.sha256(Path('output.litematic').read_bytes()).hexdigest() text='''# Delivered validation evidence This report records finite tests and samples of Hearthwright, not a count or proof of its complete design space. - **225 tests passed**, including intentional damage, public API constraints, deterministic bytes, semantic reloads, and independent client compositions. See `test-results.txt`. - **832 generated builds passed validation**: thirteen clients at each seed from 0 through 63. No failures remain in `samples/audit.json`. - **26 saved samples passed both generation and reload validation**: seeds 0 and 1 for every client. All 26 have different structural signatures and normalized placed-geometry signatures in this batch. See `samples/manifest.json`. - **107 review images**: 103 labeled sample exteriors and occupied-floor cutaways, plus four views of the default output. See `previews/index.html` and `previews/default/`. - All sixteen supplied reference source/image SHA-256 hashes still match `references/README.md`. - Temporary test files and cutaway schematics were removed. ## Observed audit diversity The last column counts only distinct normalized structural geometry observed in the 64 audited seeds for that client. It excludes furniture, vegetation and material texture noise. Plan signatures are recorded separately and are not substituted for placed-geometry evidence. | Client | Seeds tested | Observed geometry signatures | | --- | ---: | ---: | '''+ '\n'.join(rows)+''' ## Targeted checks and visual review Boundary cases include four occupied storeys, 25 by 23 footprints, seven-block storey spacing, two-block eaves, shallow and very steep roofs, hipped attics, maximum shallow-water depth, deep decks, and entrances on every cardinal wall of flat and sloping sites. The regular corpus covers joined unequal roofs, three-sided courts, irregular root interiors, buried floors, overhang supports, upper terraces and exterior stairs. Signed 64-bit seed endpoints are included in the test corpus. Mutation tests remove or obstruct stair treads, headroom, door and bed halves, stilt segments, ladder backing, glazing, roofs, lights, furniture, inventory NBT and the final terrain connection. An isolated floor pocket is rejected even when the room's anchor remains reachable. Round-trip tests reject changed names, values and explicit property sets while accepting property reordering. Exterior views and furnished cutaways were inspected across all eight reference interpretations and the five novel compositions. The review prompted shared fixes to roof closure, room-scale furnishings, root caps, lighting protection, and approaches that meet real terrain beyond a root mound or an uphill entrance. The saved images reflect the final geometry. ## Reproduce ```sh /opt/venv/bin/python -m pytest -q /opt/venv/bin/python -m scripts.audit --count 64 /opt/venv/bin/python -m scripts.sample_batch /opt/venv/bin/python -m scripts.render_batch --all-seeds /opt/venv/bin/python generate.py xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews/default ``` Default output seed: `20260925`. Default schematic SHA-256: `'''+sha+'''`. The requested library, clients, saved samples, tests, documentation, default export and review images are delivered. Runtime Minecraft physics and exact light propagation are not simulated; supported domains and further limitations are documented in `GENERATOR.md`. No exhaustive validation of arbitrary future compositions is claimed. ''' Path('VALIDATION.md').write_text(text) p=Path('GENERATOR.md');s=p.read_text();s=s.replace('## Evidence and review artifacts\n','## Evidence and review artifacts\n\nThe delivered results and observed counts are recorded in [VALIDATION.md](VALIDATION.md).\n');p.write_text(s) print('Evidence report written. Default output reload valid. Reference hashes unchanged.') PY

日志保留的输出末尾

Evidence report written. Default output reload valid. Reference hashes unchanged.
#124 · 2026-09-25T16:23:53.221Z · exit=0 · xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litem
xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews/default > previews/default-render.log 2>&1

日志保留的输出末尾

#125 · 2026-09-25T16:24:56.374Z · exit=0 · /opt/venv/bin/python - <<'PY'
/opt/venv/bin/python - <<'PY' from pathlib import Path import json, shutil required=['hearthwright/__init__.py','generate.py','output.litematic','output.json','GENERATOR.md','VALIDATION.md','samples/manifest.json','samples/audit.json','previews/index.html','test-results.txt'] assert all(Path(p).is_file() for p in required) assert '225 passed' in Path('test-results.txt').read_text() manifest=json.loads(Path('samples/manifest.json').read_text()) for sample in manifest['samples']: assert Path(sample['client']).is_file() assert (Path('samples')/sample['schematic']).is_file() assert (Path('samples')/sample['metadata']).is_file() folder=Path('previews')/(Path(sample['client']).stem+'-'+str(sample['seed'])) assert (folder/'front.png').is_file() and (folder/'rear.png').is_file() assert not list(Path('previews').rglob('*.litematic')) assert not Path('test/.tmp').exists() for root in ('hearthwright','examples','scripts','test'): for folder in Path(root).rglob('__pycache__'): shutil.rmtree(folder) for folder in (Path('__pycache__'),Path('.pytest_cache')): if folder.exists(): shutil.rmtree(folder) print('All deliverables present. All sample clients, schematics, metadata and exterior pairs accounted for. Temporary caches cleaned.') PY

日志保留的输出末尾

All deliverables present. All sample clients, schematics, metadata and exterior pairs accounted for. Temporary caches cleaned.

11 · 源码与归因

库代码 2,535 行,示例目录 344 行,测试目录 437 行,辅助脚本 154 行;另有默认客户端和渲染脚本。按交付源码统计实际行数,不用行数衡量建筑质量。

输入的八份 references/*/generate.py 保持原样,属于参考资料。harness 的通用代码统计把部分参考文件标为 edited;报告没有把它们算作本次新写的 library。浏览器桥接、控件和本报告由报告阶段添加,不归因给实验 agent。

Python 文件行数SHA-256 前缀
generate.py2280e88ac99f35c25a
hearthwright/__init__.py10ad66fcd8abe922e7
hearthwright/api.py196a3e22fdcadc16130
hearthwright/blocks.py25009125e7fc4860141
hearthwright/build.py4005eb961394e752662
hearthwright/cli.py18441dd7490c1ff2d5
hearthwright/components.py37a4d4f364aa8654c5
hearthwright/furnish.py197621e73a5fe64215d
hearthwright/materials.py30e84be33a84ad3c0c
hearthwright/navigation.py684ec51c5c7330e9ef
hearthwright/plan.py397ee24a14aeea1bf77
hearthwright/roofs.py2150ffa8cb7611144b6
hearthwright/site.py4048080c801adb3edec
hearthwright/validate.py313b8644028243b754d
examples/__init__.py16796e5d3e2f911d2
examples/canal_apothecary.py302a0c888177850e54
examples/courtyard_house.py339d7e67fbbb45848d
examples/hillside_studio.py271721efb6c4e2db50
examples/mediterranean_villa.py29425783031d4acd79
examples/merchant_manor.py317a10724ea18eeb9e
examples/orchard_house.py25aed0bbc57fdfd81a
examples/red_roof_farmstead.py227dfbf8d41d41823d
examples/root_house.py23bebfe30629c5dade
examples/sandstone_cottage.py32a4ab23b6d0e18bc6
examples/stilt_water_cabin.py22cc1beef8b97a966b
examples/stilted_manor.py213965a0d006489d37
examples/stone_cottage.py20aa58bee2e6154aaa
examples/woodland_archive.py28160805ad618d5ea8
test/conftest.py1757451c156368d39e
test/test_api.py11354c85032b11db653
test/test_corpus.py99f9e65f1ee9d12c5f
test/test_roundtrip.py885e955a1b4b5a1615
test/test_validation.py1204a29e5a5da6553f7
scripts/__init__.py11c2242595853f44f
scripts/audit.py41552837ed4135b8ae
scripts/render_batch.py63e02e0d5a4cac0b63
scripts/sample_batch.py49c49450a6477cffb8
展开阅读完整 library 与客户端源码
hearthwright/__init__.py
# Public compositional architecture, generation, transport and validation API. from .api import Choice, Span, Materials, Roof, Level, Join, Volume, Attachment, Feature, Site, Entrance, Limits, Design, DesignError from .build import Building, generate from .plan import resolve, Plan from .validate import validate, Report, Diagnostic, ValidationError, validation_rule from .blocks import blockstates_equivalent __all__ = ['Choice', 'Span', 'Materials', 'Roof', 'Level', 'Join', 'Volume', 'Attachment', 'Feature', 'Site', 'Entrance', 'Limits', 'Design', 'DesignError', 'Building', 'generate', 'resolve', 'Plan', 'validate', 'Report', 'Diagnostic', 'ValidationError', 'validation_rule', 'blockstates_equivalent', 'SiteComponent', 'register_site_component'] from .components import SiteComponent, register_site_component
hearthwright/api.py
# Architectural declarations and bounded sampling domains. from __future__ import annotations from dataclasses import dataclass, field, fields, is_dataclass from typing import Any @dataclass(frozen=True) class Choice: """Declare a finite domain of explicit alternatives. Args: values: Two or more architectural values, or one constrained value. """ values: tuple def __init__(self, *values): if not values: raise ValueError('Choice needs at least one value') object.__setattr__(self, 'values', tuple(values)) @dataclass(frozen=True) class Span: """Declare an inclusive integer domain. Args: low: Minimum value. high: Maximum value. step: Positive increment from low. """ low: int high: int step: int = 1 def __post_init__(self): if any(type(v) is not int for v in (self.low, self.high, self.step)) or self.low > self.high or self.step < 1: raise ValueError('Span requires integer low <= high and step >= 1') if len(range(self.low, self.high + 1, self.step)) > 256: raise ValueError('Span supports at most 256 declared values; narrow the domain') @dataclass(frozen=True) class Materials: """Assign coherent material roles independently of the building composition.""" wall: str = 'stone' frame: str = 'oak' roof: str = 'pale_stone' trim: str = 'spruce' floor: str = 'oak' accent: str = 'green' shutters: bool = True @dataclass(frozen=True) class Roof: """Describe a backed roof surface; ridge direction is relative to the plan.""" form: Any = 'gable' ridge: Any = 'long' pitch: Any = 1.0 eaves: int = 1 dormers: Any = 0 cupola: Any = False @dataclass(frozen=True) class Level: """Allocate domestic functions, partitioning and floor proportions.""" rooms: tuple[str, ...] = ('living', 'kitchen', 'sleeping', 'storage') layout: Any = 'open' height: int = 5 inset: int = 0 wall: str | None = None attic: bool = False @dataclass(frozen=True) class Join: """Connect a new volume to an existing volume by a shared wall and doorway.""" to: str side: Any = 'east' align: Any = 'center' level: int = 0 @dataclass(frozen=True) class Volume: """Compose an enclosed mass with levels, an envelope and vertical circulation.""" name: str width: Any = 11 depth: Any = 11 levels: tuple[Level, ...] = (Level(),) roof: Roof | None = field(default_factory=Roof) join: Join | None = None envelope: str = 'framed' circulation: Any = 'stairs' raised: int = 0 supports: str = 'foundation' root_spread: Any = 4 root_crown: Any = 5 @dataclass(frozen=True) class Attachment: """Attach usable outdoor space at a selected wall and occupied level.""" kind: str to: str side: Any = 'south' level: int = 0 depth: Any = 4 width: Any = 'full' cover: str = 'open' @dataclass(frozen=True) class Feature: """Reserve a site plot with a path to its working edge.""" kind: str side: Any = 'east' scale: Any = 7 @dataclass(frozen=True) class Site: """Define the terrain relationship and planting policy.""" terrain: str = 'meadow' margin: int = 5 planting: str = 'temperate' rise: int = 5 water_depth: int = 2 paving: str = 'garden' @dataclass(frozen=True) class Entrance: """Choose a volume, wall, level and bay for the public approach.""" volume: str side: str = 'south' level: int = 0 bay: Any = 'center' @dataclass(frozen=True) class Limits: """Bound generation cost independently of architectural dimensions.""" horizontal: int = 128 vertical: int = 96 volume: int = 1_500_000 blocks: int = 300_000 @dataclass(frozen=True) class Design: """Compose one coherent property without a building-family discriminator.""" name: str volumes: tuple[Volume, ...] materials: Materials = field(default_factory=Materials) site: Site = field(default_factory=Site) entrance: Entrance | None = None attachments: tuple[Attachment, ...] = () features: tuple[Feature, ...] = () limits: Limits = field(default_factory=Limits) class DesignError(ValueError): """Report an unsatisfiable or unsupported architectural constraint.""" def domain(value): """Expand a single bounded parameter into its declared alternatives.""" if isinstance(value, Choice): return value.values if isinstance(value, Span): return tuple(range(value.low, value.high + 1, value.step)) return (value,) def choose(value, rng, allowed=None, context='parameter'): """Sample only feasible declared choices; reject contradictory intent.""" candidates = [v for v in domain(value) if allowed is None or allowed(v)] if not candidates: raise DesignError(f'{context}: no feasible value in {domain(value)!r}') if hasattr(rng, 'pick'): return rng.pick(candidates) return candidates[rng.randrange(len(candidates))] if len(candidates) > 1 else candidates[0] def document(value): """Convert a declaration to a stable, inspectable JSON structure.""" if is_dataclass(value): return {'type': type(value).__name__, **{f.name: document(getattr(value, f.name)) for f in fields(value)}} if isinstance(value, (tuple, list)): return [document(v) for v in value] if isinstance(value, dict): return {str(k): document(v) for k, v in value.items()} return value
hearthwright/blocks.py
# Sparse block placement, authoritative states and deterministic Litematic transport. from __future__ import annotations from functools import lru_cache import json import os from pathlib import Path import random import sys MCIO_ROOT = Path(os.environ.get('MCIO_ROOT', '/work/generator/MCIO')) STATE_FILE = Path(os.environ.get('MINECRAFT_STATES', '/work/generator/MCRender/resources/1.21.1-blocks.json')) sys.path.insert(0, str(MCIO_ROOT)) from mcio.sketch import LitematicCanvas, parse_blockstate from mcio.schematic import load_schematic from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Compound, Int, String, Byte, Short, List as NBTList AIR = 'minecraft:air' @lru_cache(maxsize=1) def registry(): """Load vanilla 1.21.1 property domains and defaults from the installed authority.""" return json.loads(STATE_FILE.read_text()) @lru_cache(maxsize=None) def split_state(state): """Parse a blockstate while retaining its explicit property set.""" parsed = parse_blockstate(state).state.unpack() return parsed['Name'], parsed.get('Properties', {}) def blockstates_equivalent(left: str, right: str) -> bool: """Compare names and explicit properties, ignoring property insertion order.""" return parse_blockstate(left).state.unpack() == parse_blockstate(right).state.unpack() @lru_cache(maxsize=None) def make_state(block, pairs=()): """Fill generated states with authoritative defaults and reject invalid values.""" if '[' in block: name, props = split_state(block) return make_state(name, tuple(sorted(props.items()))) short = block.removeprefix('minecraft:') if short not in registry(): raise ValueError(f'unknown Java 1.21.1 block {block}') domains, defaults = registry()[short] props = dict(defaults) for key, value in pairs: if key not in domains or str(value) not in domains[key]: raise ValueError(f'invalid state {short}.{key}={value!r}') props[key] = str(value) return 'minecraft:' + short + ('[' + ','.join(f'{k}={v}' for k, v in sorted(props.items())) + ']' if props else '') def name(state): """Read a short block name without parsing all properties.""" return state.split('[', 1)[0].removeprefix('minecraft:') def is_air(state): return name(state) in ('air', 'cave_air', 'void_air') def passable(state): """Conservative player clearance for the vocabulary emitted by this library.""" n = name(state) return (is_air(state) or n.endswith('_door') and not n.endswith('iron_door') or n.endswith('_carpet') or n in ('ladder', 'short_grass', 'fern', 'snow') or n.endswith('_fence_gate') and split_state(state)[1].get('open') == 'true') def full_support(state): """Identify blocks with a usable full top, excluding decorative partial blocks.""" n = name(state) if is_air(state) or n in ('water', 'lava', 'ladder', 'short_grass', 'fern', 'lily_pad', 'seagrass'): return False if any(n.endswith(suffix) for suffix in ('_door', '_trapdoor', '_fence', '_fence_gate', '_wall', '_pane', '_carpet', '_bed', '_leaves', '_stairs')): return False if n.endswith('_slab'): return split_state(state)[1].get('type') in ('top', 'double') if n.startswith('potted_') or n in ('lantern', 'chain', 'flower_pot', 'torch', 'wall_torch', 'brewing_stand', 'wheat', 'poppy', 'cornflower', 'allium', 'oxeye_daisy', 'dandelion', 'sugar_cane') or 'candle' in n: return False return True def floor_support(state): return full_support(state) or name(state).endswith('_stairs') and split_state(state)[1].get('half', 'bottom') == 'bottom' def seal(state): """Require full backing for partial roof blocks; closed doors are weather seals.""" n = name(state) return full_support(state) or n.endswith('_door') and split_state(state)[1].get('open') == 'false' class Grid: """Store world coordinates sparsely and derive export bounds from final geometry.""" def __init__(self, seed=0): self.blocks = {} self.rng = random.Random(seed) self.reservations = {} self.metadata = {'schema': 1, 'rooms': [], 'routes': [], 'openings': [], 'supports': [], 'furniture': [], 'lights': [], 'interior': [], 'features': [], 'stairs': []} self.inventories = {} def get(self, x, y, z): return self.blocks.get((x, y, z), AIR) def put(self, x, y, z, block, **props): if any(type(v) is not int for v in (x, y, z)): raise ValueError(f'integer coordinates required: {(x, y, z)}') state = make_state(block, tuple(sorted(props.items()))) if is_air(state): self.blocks.pop((x, y, z), None) self.inventories.pop((x, y, z), None) else: self.blocks[x, y, z] = state def box(self, bounds, block, **props): x0, y0, z0, x1, y1, z1 = bounds if x1 < x0 or y1 < y0 or z1 < z0: return for x in range(x0, x1 + 1): for y in range(y0, y1 + 1): for z in range(z0, z1 + 1): self.put(x, y, z, block, **props) def texture(self, bounds, palette): x0, y0, z0, x1, y1, z1 = bounds for x in range(x0, x1 + 1): for y in range(y0, y1 + 1): for z in range(z0, z1 + 1): self.put(x, y, z, self.rng.choice(palette)) def reserve(self, cells, owner, purpose='passage'): for p in cells: self.reservations.setdefault(tuple(p), []).append((owner, purpose)) def route(self, points, owner, headroom=2): points = [tuple(p) for p in points] self.metadata['routes'].append({'owner': owner, 'points': points, 'headroom': headroom}) self.reserve([(x, y + dy, z) for x, y, z in points for dy in range(headroom)], owner) def free(self, cells): return all(tuple(p) not in self.reservations and is_air(self.get(*p)) for p in cells) @property def bounds(self): if not self.blocks: return (0, 0, 0, 0, 0, 0) axes = list(zip(*self.blocks)) return tuple(min(a) for a in axes) + tuple(max(a) for a in axes) def stair(self, x, y, z, material='spruce', facing='south', half='bottom'): self.put(x, y, z, material + '_stairs', facing=facing, half=half, shape='straight') def door(self, x, floor, z, side, owner, wood='spruce'): for dy, half in ((1, 'lower'), (2, 'upper')): self.put(x, floor + dy, z, wood + '_door', facing=side, half=half, hinge='left', open='false') self.metadata['openings'].append({'kind': 'door', 'owner': owner, 'cells': [(x, floor + 1, z), (x, floor + 2, z)], 'side': side}) def lantern(self, x, y, z, owner, hanging=False): self.put(x, y, z, 'lantern', hanging=str(hanging).lower()) self.metadata['lights'].append({'owner': owner, 'point': (x, y, z)}) def inventory(self, p, items): self.inventories[tuple(p)] = items def export(self, path: Path, title='Hearthwright'): """Write one region and an explicit world-coordinate design sidecar. Args: path: Destination .litematic path. title: Schematic metadata title. Returns: Path: Written file. """ path = Path(path) lo_x, lo_y, lo_z, hi_x, hi_y, hi_z = self.bounds origin = (lo_x - 1, min(0, lo_y), lo_z - 1) sx, sy, sz = hi_x - origin[0] + 2, hi_y - origin[1] + 2, hi_z - origin[2] + 2 canvas = LitematicCanvas((sy, sz, sx)) tiles = [] for (x, y, z), state in sorted(self.blocks.items()): px, py, pz = x - origin[0], y - origin[1], z - origin[2] canvas.block((py, pz, px), parse_blockstate(state)) n = name(state) kind = 'bed' if n.endswith('_bed') else n if kind in ('bed', 'barrel', 'chest', 'smoker', 'furnace'): tag = Compound({'id': String('minecraft:' + kind), 'x': Int(px), 'y': Int(py), 'z': Int(pz)}) if kind != 'bed': tag['Items'] = NBTList[Compound]([Compound({'Slot': Byte(i), 'id': String('minecraft:' + item), 'count': Int(count)}) for i, (item, count) in enumerate(self.inventories.get((x, y, z), []))]) if kind in ('smoker', 'furnace'): tag.update({'BurnTime': Short(0), 'CookTime': Short(0), 'CookTimeTotal': Short(200), 'RecipesUsed': Compound()}) tiles.append(tag) schematic = canvas.to_litematica(name=title, author='Hearthwright', description='Composed architecture; design and validation metadata in adjacent JSON.', minecraft_data_version=3955) schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 nbt = schematic.write_to_nbt() region = next(iter(nbt['Regions'].values())) region['TileEntities'] = NBTList[Compound](tiles) path.parent.mkdir(parents=True, exist_ok=True) NBTFile(nbt).save(path) self.metadata['origin'] = origin self.metadata['size_xyz'] = (sx, sy, sz) self.metadata['inventories'] = [{'point': p, 'items': items} for p, items in sorted(self.inventories.items())] path.with_suffix('.json').write_text(json.dumps(self.metadata, indent=2, sort_keys=True) + '\n') return path def verify(self, path: Path): """Compare the entire reloaded non-air volume semantically, including extra cells.""" other = Grid.load(path) if set(self.blocks) != set(other.blocks): raise AssertionError('non-air coordinate set changed on reload') for p, state in self.blocks.items(): if not blockstates_equivalent(state, other.blocks[p]): raise AssertionError(f'cell {p} changed on reload: {state!r} -> {other.blocks[p]!r}') return {'blocks': len(self.blocks), 'states': len(set(self.blocks.values())), 'size_xyz': other.metadata['size_xyz']} @classmethod def load(cls, path: Path, metadata: dict | Path | None = None): """Reload geometry with explicit metadata, defaulting to the adjacent sidecar.""" path = Path(path) if metadata is None: metadata = path.with_suffix('.json') if isinstance(metadata, (str, Path)): metadata = json.loads(Path(metadata).read_text()) grid = cls() grid.metadata = metadata ox, oy, oz = metadata['origin'] schematic = load_schematic(path) array = schematic.read_flat(0, schematic.volume).reshape(schematic.size_yzx) for index, state in enumerate(schematic.palette): if is_air(state): continue import numpy as np for y, z, x in zip(*np.where(array == index)): grid.blocks[int(x) + ox, int(y) + oy, int(z) + oz] = state for route in metadata['routes']: grid.reserve([(x, y + dy, z) for x, y, z in route['points'] for dy in range(route['headroom'])], route['owner']) nbt = NBTFile.load_regardless_of_gzipped(path) region = next(iter(nbt['Regions'].values())) tiles = region.get('TileEntities') for tag in tiles.unpack() if tiles is not None else []: if 'Items' in tag: point = tag['x'] + ox, tag['y'] + oy, tag['z'] + oz grid.inventories[point] = [(item['id'].removeprefix('minecraft:'), item['count']) for item in sorted(tag['Items'], key=lambda item: item['Slot'])] return grid
hearthwright/build.py
# Compile a resolved architectural plan into furnished, reserved block geometry. from __future__ import annotations from collections import deque from dataclasses import asdict import hashlib import json from pathlib import Path from .api import Design, DesignError from .blocks import Grid, is_air, passable, name, full_support, split_state from .materials import WALLS, DIRECTIONS, OPPOSITE, log from .plan import resolve, edge_point from . import site, roofs from .navigation import flood, path_to, walkable class Compiler: """Share construction state and spatial reservations across reusable components.""" def __init__(self, plan): self.plan = plan self.g = Grid(plan.seed) self.ground = {} self.interior = set() self.inner = {} self.rooms = [] self.start = None self.roof_maps = {} for mass in plan.masses: for s in mass.levels: self.inner[s.id] = {(x, z) for x, z in s.cells if all((x + dx, z + dz) in s.cells for dx, dz in DIRECTIONS.values())} def opening_route(self, p, side, owner): x, y, z = p dx, dz = DIRECTIONS[side] points = [(x + dx * d, y, z + dz * d) for d in (-1, 0, 1)] self.g.route(points, owner) def construct(self): """Run geometry passes in dependency order, reserving circulation before furniture.""" site.terrain(self) for m in self.plan.masses: if m.envelope == 'roots': roofs.root_envelope(self, m) for m in self.plan.masses: self.envelope(m) roofs.build_roofs(self) for m in self.plan.masses: self.partitions(m) self.circulation(m) self.openings() for a in self.plan.attachments: site.attachment(self, a) site.approach(self) for f in self.plan.features: site.feature(self, f) site.connect_features(self) site.approach_lighting(self) self.reserve_rooms() structural_geometry = self.geometry_signature() from .furnish import furnish furnish(self) site.planting(self) self.connect_fences() self.g.metadata['interior'] = sorted(self.interior) self.g.metadata['resolved'] = self.plan.summary() self.g.metadata['structural_signature'] = self.plan.signature() self.g.metadata['geometry_signature'] = structural_geometry self.g.metadata['limits'] = asdict(self.plan.design.limits) self.g.metadata['seed'] = self.plan.seed self.g.metadata['name'] = self.plan.design.name self.check_limits() return self.g def envelope(self, mass): """Excavate, frame and seal arbitrary stacked footprints on real foundations.""" g, mat = self.g, self.plan.design.materials base = mass.levels[0] if mass.supports == 'foundation': for x, z in sorted(base.cells): g.box((x, 1, z, x, base.floor - 1, z), 'stone_bricks') else: r = base.rect posts = {(r.x0, r.z0), (r.x1, r.z0), (r.x0, r.z1), (r.x1, r.z1)} for x in range(r.x0, r.x1 + 1, 5): posts |= {(x, r.z0), (x, r.z1)} for z in range(r.z0, r.z1 + 1, 5): posts |= {(r.x0, z), (r.x1, z)} for x, z in sorted(posts): site.column(self, x, z, base.floor - 1, mass.name) for z in (r.z0, r.z1): g.box((r.x0, base.floor - 1, z, r.x1, base.floor - 1, z), log(mat.frame), axis='x') for i, s in enumerate(mass.levels): inner = self.inner[s.id] for x, z in sorted(s.cells): g.put(x, s.floor, z, mat.floor + '_planks' if (x, z) in inner else 'stone_bricks' if i == 0 else log(mat.frame)) if (x, z) in inner: g.box((x, s.floor + 1, z, x, s.floor + s.height - 1, z), 'air') self.interior.update((x, y, z) for y in range(s.floor + 1, s.floor + s.height)) else: for y in range(s.floor + 1, s.floor + s.height): # Root envelopes use a continuous irregular stone lining. frame = mass.envelope == 'framed' and ((x in (s.rect.x0, s.rect.x1) and (z - s.rect.z0) % 5 == 0) or (z in (s.rect.z0, s.rect.z1) and (x - s.rect.x0) % 5 == 0)) g.put(x, y, z, log(mat.frame) if frame else g.rng.choice(WALLS[s.wall]), **({'axis': 'y'} if frame else {})) if not s.attic: ceiling = s.floor + s.height if mass.envelope == 'roots' and i == len(mass.levels) - 1: depth = min(x - s.rect.x0, s.rect.x1 - x, z - s.rect.z0, s.rect.z1 - z) # A dark organic cap buries the ceiling into the trunk shoulder. cap = ceiling + 1 + min(3, depth) for y in range(ceiling, cap): if is_air(g.get(x, y, z)) or y == ceiling: g.put(x, y, z, 'dark_oak_log', axis='y') if depth < 2 and (x + 2 * z) % 5 <= 1 and is_air(g.get(x, cap, z)): g.put(x, cap, z, 'moss_block') else: g.put(x, ceiling, z, mat.floor + '_planks') # Full perimeter sill and top beam make the frame readable from every side. if mass.envelope == 'framed': for x, z in sorted(s.cells - inner): g.put(x, s.floor + s.height - 1, z, log(mat.frame), axis='x' if z in (s.rect.z0, s.rect.z1) else 'z') if i and s.cells - mass.levels[i - 1].cells: below = mass.levels[i - 1] # An upper overhang is carried by full posts and high knee braces. r = s.rect points = {(r.x0, r.z0), (r.x1, r.z0), (r.x0, r.z1), (r.x1, r.z1), (r.x0 + r.width // 3, r.z0), (r.x0 + r.width // 3, r.z1), (r.x1 - r.width // 3, r.z0), (r.x1 - r.width // 3, r.z1)} points.update((x, z) for x in (r.x0, r.x1) for z in (r.z0 + r.depth // 3, r.z1 - r.depth // 3)) for x, z in sorted(points): if (x, z) in below.cells: continue site.column(self, x, z, s.floor - 1, s.id) for dx, dz in DIRECTIONS.values(): if (x + dx, z + dz) in s.cells and (x + dx, z + dz) not in below.cells: g.stair(x + dx, s.floor - 1, z + dz, mat.floor, next(k for k, delta in DIRECTIONS.items() if delta == (-dx, -dz)), 'top') g.stair(x + dx, s.floor - 2, z + dz, mat.floor, next(k for k, delta in DIRECTIONS.items() if delta == (-dx, -dz)), 'top') for z in (r.z0, r.z1): g.box((r.x0, s.floor - 1, z, r.x1, s.floor - 1, z), log(mat.frame), axis='x') for x in (r.x0, r.x1): g.box((x, s.floor - 1, r.z0, x, s.floor - 1, r.z1), log(mat.frame), axis='z') def partitions(self, mass): """Subdivide selected levels into functional rooms with real door connections.""" g = self.g for s in mass.levels: inner = self.inner[s.id] groups = [inner] programs = [s.functions] if s.layout != 'open': axis = 0 if s.layout == 'cross' else 1 middle = s.rect.center[axis] wall = {(x, z) for x, z in inner if (x, z)[axis] == middle} for x, z in sorted(wall): ceiling = self.roof_maps.get(mass.name, {}).get((x, z), {}).get('height', s.floor + s.height) if s.attic else s.floor + s.height g.box((x, s.floor + 1, z, x, ceiling - 1, z), 'smooth_sandstone' if s.wall in ('plaster', 'white') else 'oak_planks') cx, cz = s.rect.center g.door(cx, s.floor, cz, 'east' if axis == 0 else 'south', s.id) self.opening_route((cx, s.floor + 1, cz), 'east' if axis == 0 else 'south', s.id) groups = [{p for p in inner if p[axis] < middle}, {p for p in inner if p[axis] > middle}] split = (len(s.functions) + 1) // 2 programs = [s.functions[:split], s.functions[split:]] for index, (cells, functions) in enumerate(zip(groups, programs)): self.rooms.append({'id': f'{s.id}/room{index}', 'level': s.id, 'floor': s.floor, 'height': s.height, 'cells': cells, 'functions': functions}) def circulation(self, mass): """Build complete stairwells or backed ladders between consecutive floor masks.""" g = self.g for flight, (low, high) in enumerate(zip(mass.levels, mass.levels[1:])): common = self.inner[low.id] & self.inner[high.id] owner = f'{low.id}->{high.id}' delta = high.floor - low.floor protected = set() openings = [m.join for m in self.plan.masses if m.join] openings.append(self.plan.entrance) openings += [{'point': a['door'], 'floor': a['floor'], 'side': a['side']} for a in self.plan.attachments] for opening in openings: if opening['floor'] not in (low.floor, high.floor): continue px, pz = opening['point'] dx, dz = DIRECTIONS[opening['side']] protected.update((px + dx * d, pz + dz * d) for d in (-1, 0, 1)) if mass.circulation == 'ladder': candidates = [(x, z) for x, z in sorted(common) if (x, z) not in protected and all((x, yy, z) in self.interior for yy in (low.floor + 1, high.floor + 1)) and (x - 1, z) not in common] if not candidates: raise DesignError(f'{owner}: no shared ladder shaft') x, z = min(candidates, key=lambda p: abs(p[1] - low.rect.center[1])) g.box((x - 1, low.floor + 1, z, x - 1, high.floor + 2, z), 'spruce_planks') for y in range(low.floor + 1, high.floor + 2): g.put(x, y, z, 'ladder', facing='east') g.put(x, high.floor + 2, z, 'air') self.interior.update((x, y, z) for y in range(low.floor + 1, high.floor + 3)) g.route([(x, y, z) for y in range(low.floor + 1, high.floor + 2)], owner) continue candidates = [(x, z) for x, z in sorted(common) if all((xx, zz) in common and (xx, zz) not in protected for xx in (x, x + 1) for zz in range(z - 1, z + delta + 1))] if not candidates: raise DesignError(f'{owner}: no shared stair rectangle with both landings') x, z0 = min(candidates, key=lambda q: (-q[0] if flight % 2 else q[0], q[1])) points = [] for i in range(delta): z = z0 + i y = low.floor + i + 1 for xx in (x, x + 1): if (xx, z) not in common: raise DesignError(f'{owner}: staircase exceeds the shared floor, use ladder or enlarge depth') g.box((xx, low.floor + 1, z, xx, y - 1, z), 'spruce_planks') g.stair(xx, y, z, 'spruce', 'south') g.box((xx, y + 1, z, xx, y + 3, z), 'air') self.interior.update((xx, yy, z) for yy in range(y, y + 4)) points.append((xx, y + 1, z)) g.metadata['stairs'].append({'owner': owner, 'point': (xx, y, z), 'facing': 'south'}) for yy, z in ((low.floor + 1, z0 - 1), (high.floor + 1, z0 + delta)): for xx in (x, x + 1): g.box((xx, yy, z, xx, yy + 2, z), 'air') points.append((xx, yy, z)) # Guard the upper opening without blocking the top or bottom landing. for z in range(z0 + max(1, delta - 3), z0 + delta): if is_air(g.get(x + 2, high.floor + 1, z)): g.put(x + 2, high.floor + 1, z, 'spruce_fence') g.route(points, owner) def openings(self): """Align glazed bays with all free facades, then cut registered doors.""" g, p, mat = self.g, self.plan, self.plan.design.materials doors = [] for m in p.masses: if m.join: j = m.join doors.append((j['point'], j['floor'], j['side'], m.name + '/join')) e = p.entrance doors.append((e['point'], e['floor'], e['side'], e['volume'] + '/entrance')) for a in p.attachments: doors.append((a['door'], a['floor'], a['side'], a['id'])) for m in p.masses: for s in m.levels: r = s.rect for side in DIRECTIONS: dx, dz = DIRECTIONS[side] span = r.width if dz else r.depth offsets = [span // 2] if span <= 11 else [span // 3, 2 * span // 3] for offset in offsets: x, z = ((r.x0 + offset, r.z1 if side == 'south' else r.z0) if dz else (r.x1 if side == 'east' else r.x0, r.z0 + offset)) if (x, z) not in s.cells or (x - dx, z - dz) not in self.inner[s.id]: continue if any(abs(x - q[0]) + abs(z - q[1]) < 3 and floor == s.floor for q, floor, _, _ in doors): continue if any(other.name != m.name and any(l.rect.contains(x + dx, z + dz) and l.floor <= s.floor + 2 <= l.floor + l.height for l in other.levels) for other in p.masses): continue yy = s.floor + 2 points = [(x, yy, z), (x, yy + 1, z)] if s.height >= 5 else [(x, yy, z)] if any(not passable(g.get(x - dx, py, z - dz)) for _, py, _ in points): continue for pt in points: g.put(*pt, 'light_blue_stained_glass' if mat.wall == 'white' else 'glass') g.metadata['openings'].append({'kind': 'window', 'owner': s.id, 'cells': points, 'side': side}) # Daylight wells through thick hills preserve the original sealed window. distance = m.roots['spread'] + 2 if m.envelope == 'roots' else 2 if p.design.site.terrain == 'slope' else 1 for d in range(1, distance + 1): px, pz = x + dx * d, z + dz * d g.box((px, yy, pz, px, yy + len(points) - 1, pz), 'air') g.reserve([(x - dx, y, z - dz) for _, y, _ in points], s.id, 'window') if mat.shutters and m.envelope != 'roots' and s.height >= 5: tx, tz = (1, 0) if dz else (0, 1) for sign in (-1, 1): for y in range(yy, yy + len(points)): px, pz = x + dx + tx * sign, z + dz + tz * sign if is_air(g.get(px, y, pz)): g.put(px, y, pz, ('warped' if mat.wall == 'white' else mat.frame) + '_trapdoor', facing=side, open='true') # Small flower box attaches to the wall under the sill. if m.envelope != 'roots' and all(is_air(g.get(x + d * dx, s.floor, z + d * dz)) for d in (1, 2)) and not any(a['floor'] <= s.floor <= a['floor'] + (6 if a['cover'] != 'open' else 1) and a['rect'].contains(x + dx, z + dz) for a in p.attachments): g.put(x + dx, s.floor, z + dz, 'dirt') g.put(x + dx, s.floor + 1, z + dz, 'cornflower' if mat.accent in ('blue', 'cyan') else 'poppy') g.put(x + 2 * dx, s.floor, z + 2 * dz, mat.frame + '_trapdoor', facing=side, open='true') g.stair(x + dx, s.floor - 1, z + dz, mat.floor, OPPOSITE[side], 'top') for (x, z), floor, side, owner in doors: g.door(x, floor, z, side, owner, 'warped' if mat.wall == 'white' else 'spruce') dx, dz = DIRECTIONS[side] for distance in (-1, 1): xx, zz = x + dx * distance, z + dz * distance g.box((xx, floor + 1, zz, xx, floor + 2, zz), 'air') if is_air(g.get(xx, floor, zz)) or name(g.get(xx, floor, zz)) == 'water': g.put(xx, floor, zz, mat.floor + '_planks') self.opening_route((x, floor + 1, z), side, owner) # Recessed entrance surround projects one block and stays three blocks high. x, z = e['point'] y = e['floor'] dx, dz = DIRECTIONS[e['side']] for w in (-2, 2): xx, zz = x + dx + (w if dz else 0), z + dz + (w if dx else 0) g.box((xx, y, zz, xx, y + 3, zz), log(mat.frame) if mat.wall != 'white' else 'smooth_sandstone') for w in range(-2, 3): xx, zz = x + dx + (w if dz else 0), z + dz + (w if dx else 0) g.put(xx, y + 3, zz, mat.floor + '_slab', type='top') # Lamp beside the doorway is supported by a real sill. xx, zz = x + dx + (2 if dz else 0), z + dz + (2 if dx else 0) g.lantern(xx, y + 4, zz, 'entrance') def reserve_rooms(self): """Derive protected paths through the actual empty structure before decoration.""" g = self.g parents = flood(g, self.start) if tuple(g.metadata['entrance']) not in parents: raise DesignError('approach does not reach entrance in actual geometry') for room in self.rooms: cx = sum(x for x, z in room['cells']) / len(room['cells']) cz = sum(z for x, z in room['cells']) / len(room['cells']) candidates = [(x, room['floor'] + 1, z) for x, z in room['cells'] if (x, room['floor'] + 1, z) in parents] if not candidates: raise DesignError(f'{room["id"]}: no reachable floor; review its circulation or join') anchor = min(candidates, key=lambda p: (abs(p[0] - cx) + abs(p[2] - cz), p)) room['anchor'] = anchor g.route(path_to(parents, anchor), room['id']) g.metadata['rooms'].append({**room, 'cells': sorted(room['cells'])}) for f in g.metadata['features']: if tuple(f['target']) not in parents: raise DesignError(f'{f["owner"]}: outdoor target is not reachable') g.route(path_to(parents, f['target']), f['owner']) def connect_fences(self): """Resolve explicit fence connections so exports render and paste consistently.""" g = self.g for (x, y, z), state in list(g.blocks.items()): if name(state).endswith('_fence'): properties = {} for face, (dx, dz) in DIRECTIONS.items(): n = name(g.get(x + dx, y, z + dz)) properties[face] = str(n.endswith(('_fence', '_log', '_planks', '_wood'))).lower() g.put(x, y, z, name(state), **properties) # Connection properties are finalized after all neighboring furniture exists. for group in g.metadata['furniture']: for i, expected in enumerate(group['states']): if name(expected).endswith('_fence'): group['states'][i] = g.get(*group['cells'][i]) def geometry_signature(self): """Digest actual structural cells, excluding texture, plants and cosmetic materials.""" # Snapshot before furnishing and planting; normalize all structural material names. selected = [] for mass in self.plan.masses: r = mass.rect.inset(-2) bottom = mass.levels[0].floor for (x, y, z), state in sorted(self.g.blocks.items()): if not r.contains(x, z) or y < bottom: continue n = name(state) role = None if n.endswith('_door') or n.endswith('_stairs') or n == 'ladder': properties = split_state(state)[1] role = ('door' if n.endswith('_door') else 'stair' if n.endswith('_stairs') else 'ladder') + ':' + properties.get('facing', '') + ':' + properties.get('half', '') elif 'glass' in n: role = 'glass' elif n.endswith('_slab'): role = 'slab:' + split_state(state)[1].get('type', '') elif full_support(state): role = 'solid' if role: selected.append((x, y, z, role)) return hashlib.sha256(json.dumps(selected, separators=(',', ':')).encode()).hexdigest() def check_limits(self): b = self.g.bounds size = [b[i + 3] - b[i] + 3 for i in range(3)] limits = self.plan.design.limits if max(size[0], size[2]) > limits.horizontal or size[1] > limits.vertical or size[0] * size[1] * size[2] > limits.volume or len(self.g.blocks) > limits.blocks: raise DesignError(f'final geometry size {size} / {len(self.g.blocks)} blocks exceeds Limits; increase resource limits or reduce the composition') class Building: """Expose a generated artifact, design metadata, validation and deterministic export.""" def __init__(self, grid, plan=None): self.grid = grid self.plan = plan @property def metadata(self): return self.grid.metadata def validate(self): from .validate import validate return validate(self) def export(self, path: Path): self.validate().require_valid() return self.grid.export(Path(path), self.metadata['name']) def verify(self, path: Path): return self.grid.verify(Path(path)) @classmethod def load(cls, path: Path, metadata=None): return cls(Grid.load(Path(path), metadata)) def generate(design: Design, seed: int, *, validate=True) -> Building: """Resolve, construct and optionally validate one user-composed property.""" plan = resolve(design, seed) artifact = Building(Compiler(plan).construct(), plan) if validate: artifact.validate().require_valid() return artifact
hearthwright/cli.py
# Optional shared CLI transport; architectural clients also work through the Python API. import argparse from pathlib import Path from .build import generate DEFAULT_SEED = 20260925 def run(design, *, default_seed=DEFAULT_SEED): """Generate, validate, export and semantically verify a declared property.""" parser = argparse.ArgumentParser(description=design.name) parser.add_argument('--seed', type=int, default=default_seed) parser.add_argument('--output', type=Path, default=Path('output.litematic')) args = parser.parse_args() building = generate(design, args.seed) building.export(args.output) print(f'wrote {args.output}: {building.verify(args.output)}; validation passed')
hearthwright/components.py
# Extension contracts for reusable site components with shared planning and validation. from dataclasses import dataclass from typing import Callable @dataclass(frozen=True) class SiteComponent: """Register a reusable geometry algorithm, not a complete building preset. Args: name: Unique capability identifier used by Feature. realize: Callable receiving Compiler context and the resolved feature dictionary. It must set feature['target'] to an accessible feet position, append a metadata feature with owner/kind/target, and record supports/reservations. scale: Inclusive minimum and maximum plot width/depth. rule: Optional validator receiving Grid, metadata and Report. version: Explicit implementation version retained in the resolved plan. """ name: str realize: Callable scale: tuple[int, int] = (5, 11) rule: Callable | None = None version: str = '1' SITE_COMPONENTS = {} BUILTIN_FEATURES = ('pool', 'field', 'well', 'windmill', 'landing', 'garden') def register_site_component(component: SiteComponent): """Integrate a component with plot allocation, route planning and validation.""" if component.name in SITE_COMPONENTS or component.name in BUILTIN_FEATURES: raise ValueError(f'component name already registered: {component.name}') if not 3 <= component.scale[0] <= component.scale[1] <= 15: raise ValueError('component plot scale must lie in 3..15') SITE_COMPONENTS[component.name] = component return component
hearthwright/furnish.py
# Fit reusable domestic furniture groups around reserved circulation and daylight. from collections import deque from .api import DesignError from .blocks import is_air, full_support from .materials import DIRECTIONS from .navigation import flood, path_to def table_group(wood): """Build a one-block-high trestle table with four inward-facing chairs.""" pieces = [(0, 0, 0, wood + '_stairs', {'facing': 'east', 'half': 'top'}), (2, 0, 0, wood + '_stairs', {'facing': 'west', 'half': 'top'}), (1, 0, 0, wood + '_slab', {'type': 'top'}), (1, 1, 0, 'flower_pot', {})] pieces += [(x, 0, z, wood + '_stairs', {'facing': 'north' if z < 0 else 'south'}) for x in (0, 2) for z in (-1, 1)] return pieces def modules(function, mat, area): """Translate room functions into useful compact furniture groups.""" color = mat.accent sofa = [(0, 0, 0, mat.floor + '_stairs', {'facing': 'north'}), (1, 0, 0, mat.floor + '_stairs', {'facing': 'north'})] programs = { 'living': [('settle', sofa), ('table', [(0, 0, 0, mat.floor + '_planks', {}), (0, 1, 0, 'potted_fern', {})])], 'kitchen': [('cooking', [(0, 0, 0, 'smoker', { 'facing': 'south' }), (1, 0, 0, 'crafting_table', {}), (2, 0, 0, 'water_cauldron', { 'level': '3' })])], 'sleeping': [('bed', [(0, 0, 0, color + '_bed', { 'part': 'foot', 'facing': 'north' }), (0, 0, -1, color + '_bed', { 'part': 'head', 'facing': 'north' })]), ('bedside', [(0, 0, 0, 'barrel', { 'facing': 'up' })])], 'storage': [('stores', [(0, 0, 0, 'barrel', { 'facing': 'south' }), (1, 0, 0, 'barrel', { 'facing': 'south' })])], 'workshop': [('workbench', [(0, 0, 0, 'crafting_table', {}), (1, 0, 0, 'smithing_table', {})]), ('toolbox', [(0, 0, 0, 'barrel', { 'facing': 'up' })])], 'study': [('bookcase', [(0, 0, 0, 'bookshelf', {}), (0, 1, 0, 'bookshelf', {})]), ('desk', [(0, 0, 0, 'cartography_table', {})])], 'enchanting': [('enchantment', [(0, 0, 0, 'enchanting_table', {})]), ('books', [(0, 0, 0, 'bookshelf', {}), (1, 0, 0, 'bookshelf', {})])], 'brewing': [('alchemy', [(0, 0, 0, 'smooth_stone', {}), (0, 1, 0, 'brewing_stand', {}), (1, 0, 0, 'water_cauldron', { 'level': '3' })])], 'trading': [('counter', [(0, 0, 0, 'barrel', { 'facing': 'south' }), (1, 0, 0, 'cartography_table', {})])], 'fishing': [('tackle', [(0, 0, 0, 'barrel', { 'facing': 'up' }), (1, 0, 0, 'smoker', { 'facing': 'south' })])], } if area >= 75: if function == 'living': programs[function] = [ ('settle', [(x, 0, 0, mat.floor + '_stairs', { 'facing': 'north' }) for x in range(3)] + [(3, 0, 0, mat.floor + '_planks', {}), (3, 1, 0, 'potted_fern', {})]), ('dining', table_group(mat.floor)), ] if function == 'kitchen': programs[function] = [('kitchen_counter', programs[function][0][1] + [(3, 0, 0, 'barrel', {'facing': 'south'}), (3, 1, 0, 'potted_red_tulip', {})]), ('cupboard', [(0, 0, 0, 'barrel', {'facing': 'south'}), (0, 1, 0, 'barrel', {'facing': 'south'})])] if function == 'sleeping': programs[function] = [('double_bed', [(x, 0, z, color + '_bed', {'part': part, 'facing': 'north'}) for x in (0, 1) for z, part in ((0, 'foot'), (-1, 'head'))]), ('wardrobe', [(0, 0, 0, 'barrel', {'facing': 'south'}), (0, 1, 0, 'barrel', {'facing': 'south'}), (1, 0, 0, 'bookshelf', {})])] if function == 'storage': programs[function] = [('stores', [(x, y, 0, 'barrel', {'facing': 'south'}) for x in range(3) for y in (0, 1)])] if function == 'study': programs[function] = [('library', [(x, y, 0, 'bookshelf', {}) for x in range(3) for y in (0, 1)]), ('writing_desk', [(0, 0, 0, 'cartography_table', {}), (1, 0, 0, mat.floor + '_planks', {}), (1, 1, 0, 'potted_fern', {}), (0, 0, 1, mat.floor + '_stairs', {'facing': 'south'})])] if function == 'enchanting': programs[function] = [('enchantment', [(0, 0, 0, 'enchanting_table', {})]), ('books', [(x, y, 0, 'bookshelf', {}) for x in range(3) for y in (0, 1)])] if area >= 150 and function == 'living': stove = [(x, y, 0, 'stone_bricks', {}) for x in (0, 2) for y in (0, 1)] stove += [(1, 0, 0, 'furnace', {'facing': 'south'}), (1, 1, 0, 'chiseled_stone_bricks', {})] stove += [(x, 2, 0, 'stone_brick_slab', {'type': 'top'}) for x in range(3)] programs[function].append(('masonry_stove', stove)) programs[function].append(('sideboard', [(0, 0, 0, 'barrel', {'facing': 'south'}), (1, 0, 0, 'bookshelf', {}), (1, 1, 0, 'potted_fern', {})])) if area >= 150: if function == 'sleeping': # Bedside furniture belongs to the bed, with space at its foot for approach. bed = programs[function][0][1] bed += [(x, 0, -1, 'barrel', {'facing': 'south'}) for x in (-1, 2)] bed += [(-1, 1, -1, 'potted_fern', {}), (2, 1, -1, 'lantern', {'hanging': 'false'})] programs[function] += [ ('linen_chest', [(x, 0, 0, 'barrel', { 'facing': 'up' }) for x in range(3)]), ('dressing_table', [(x, 0, 0, mat.floor + '_planks', {}) for x in range(3)] + [(0, 1, 0, 'potted_red_tulip', {}), (2, 1, 0, 'flower_pot', {}), (1, 0, 1, mat.floor + '_stairs', { 'facing': 'south' })]), ] if function == 'study': programs[function].append(('reference_shelves', [(x, y, 0, 'bookshelf', {}) for x in range(4) for y in (0, 1)])) programs[function].append(('reading_table', table_group(mat.floor))) if function == 'kitchen': programs[function].append(('pantry', [(x, y, 0, 'barrel', {'facing': 'south'}) for x in range(3) for y in (0, 1)])) programs[function].append(('island', [(x, 0, 0, 'smooth_stone', {}) for x in range(3)] + [(0, 1, 0, 'potted_fern', {}), (2, 1, 0, 'flower_pot', {})])) if function == 'workshop': programs[function].append(('long_workbench', [(x, 0, 0, block, {}) for x, block in enumerate(('crafting_table', 'fletching_table', 'smithing_table', 'cartography_table'))])) programs[function].append(('parts_shelves', [(x, y, 0, 'barrel', {'facing': 'south'}) for x in range(3) for y in (0, 1)])) if function == 'storage': programs[function].append(('supply_rack', [(x, y, 0, 'barrel', {'facing': 'south'}) for x in range(4) for y in (0, 1)])) return programs[function] def rotate_piece(piece, rotation): """Rotate a furnishing in plan, including bed halves and chair facings.""" dx, dy, dz, block, properties = piece props = dict(properties) for _ in range(rotation): dx, dz = -dz, dx if 'facing' in props and props['facing'] in DIRECTIONS: props['facing'] = {'north': 'east', 'east': 'south', 'south': 'west', 'west': 'north'}[props['facing']] return dx, dy, dz, block, props def place_group(ctx, room, kind, pieces, function): """Find a supported layout and reserve a reachable interaction point before placement.""" g = ctx.g floor = room['floor'] cells = room['cells'] allowed = {(x, floor + 1, z) for x, z in cells} original_reachable = set(flood(g, room['anchor'], allowed, maximum=2000)) candidates = [(x, z, rot) for x, z in sorted(cells) for rot in range(4)] g.rng.shuffle(candidates) # Favor corners and walls, while retaining randomized tie-breaks. if kind in ('dining', 'reading_table', 'island'): candidates.sort(key=lambda p: abs(p[0] - room['anchor'][0]) + abs(p[1] - room['anchor'][2])) else: candidates.sort(key=lambda p: sum((p[0] + dx, p[1] + dz) in cells for dx, dz in DIRECTIONS.values())) for x, z, rotation in candidates: parts = [rotate_piece(piece, rotation) for piece in pieces] footprint = {(x + dx, z + dz) for dx, _, dz, _, _ in parts} if not footprint <= cells: continue occupied = [(x + dx, floor + 1 + dy, z + dz) for dx, dy, dz, _, _ in parts] clearance = [(px, floor + dy, pz) for px, pz in footprint for dy in (1, 2)] if not g.free(occupied) or not g.free(clearance) or any(not full_support(g.get(px, floor, pz)) for px, pz in footprint): continue parents = flood(g, room['anchor'], allowed - {(px, floor + 1, pz) for px, pz in footprint}, maximum=2000) removed = {(px, floor + 1, pz) for px, pz in footprint} if original_reachable - removed - set(parents): continue contacts = [(px + dx, floor + 1, pz + dz) for px, pz in sorted(footprint) for dx, dz in DIRECTIONS.values() if (px + dx, floor + 1, pz + dz) in parents] if not contacts: continue access = min(contacts, key=lambda p: (abs(p[0] - room['anchor'][0]) + abs(p[2] - room['anchor'][2]), p)) g.route(path_to(parents, access), room['id'] + '/' + kind) for dx, dy, dz, block, props in parts: point = x + dx, floor + 1 + dy, z + dz g.put(*point, block, **props) if block in ('barrel', 'smoker'): supplies = [('fishing_rod', 1), ('cod', 6), ('string', 12)] if function == 'fishing' else [('emerald', 8), ('paper', 12)] if function == 'trading' else [('bread', 6), ('oak_log', 12)] g.inventory(point, supplies) g.metadata['furniture'].append({'owner': room['id'], 'function': function, 'kind': kind, 'cells': occupied, 'access': access, 'states': [g.get(*p) for p in occupied]}) return raise DesignError(f'{room["id"]}: cannot fit {function}/{kind} with clear access; enlarge the floor or reduce this room program') def furnish(ctx): """Equip every program, illuminate every room and keep the protected routes clear.""" g, mat = ctx.g, ctx.plan.design.materials for room in ctx.rooms: # Fit long and multi-block pieces first, before optional decor. groups = [(kind, pieces, function) for function in room['functions'] for kind, pieces in modules(function, mat, len(room['cells']))] groups.sort(key=lambda item: -len({(p[0], p[2]) for p in item[1]})) for kind, pieces, function in groups: place_group(ctx, room, kind, pieces, function) # The lamp module carries its own real support, including in irregular attics. place_group(ctx, room, 'lamp', [(0, 0, 0, mat.floor + '_planks', {}), (0, 1, 0, 'lantern', {'hanging': 'false'})], 'lighting') lamp = g.metadata['furniture'][-1]['cells'][-1] g.metadata['lights'].append({'owner': room['id'], 'point': lamp}) if len(room['cells']) > 100 and room['height'] >= 5: # Two supported pendant lamps illuminate larger domestic rooms. ax, _, az = room['anchor'] candidates = sorted(room['cells'], key=lambda p: abs(p[0] - ax) + abs(p[1] - az)) pendants = [] for x, z in candidates: y = room['floor'] + room['height'] - 1 if any(abs(x - px) + abs(z - pz) < 6 for px, pz in pendants): continue if full_support(g.get(x, y + 1, z)) and g.free([(x, y, z)]): g.lantern(x, y, z, room['id'], True) pendants.append((x, z)) if len(pendants) == 2: break # Inlaid rugs add domestic color without reducing headroom or obstructing steps. ax, _, az = room['anchor'] for x in range(ax - 2, ax + 3): for z in range(az - 1, az + 2): if (x, z) in room['cells'] and full_support(g.get(x, room['floor'], z)) and is_air(g.get(x, room['floor'] + 1, z)): g.put(x, room['floor'], z, (mat.accent if abs(x - ax) == 2 or abs(z - az) == 1 else 'white') + '_terracotta')
hearthwright/materials.py
# Harmonious material-role vocabularies shared by every composition. WALLS = { 'stone': ('andesite', 'andesite', 'stone', 'cobblestone'), 'masonry': ('stone_bricks', 'stone_bricks', 'andesite', 'mossy_stone_bricks'), 'plaster': ('calcite',) * 12 + ('smooth_sandstone',), 'white': ('smooth_quartz', 'smooth_quartz', 'calcite'), 'blue': ('light_blue_concrete', 'light_blue_concrete', 'light_blue_terracotta'), 'timber': ('oak_planks', 'oak_planks', 'stripped_oak_wood'), 'earth': ('tuff', 'mossy_cobblestone', 'andesite'), } # Full backing, stair, slab: real vanilla block names, never synthesized stone plurals. ROOFS = { 'pale_stone': ('andesite', 'andesite_stairs', 'andesite_slab'), 'sandstone': ('smooth_sandstone', 'smooth_sandstone_stairs', 'smooth_sandstone_slab'), 'slate': ('deepslate_tiles', 'deepslate_tile_stairs', 'deepslate_tile_slab'), 'gray': ('stone_bricks', 'stone_brick_stairs', 'stone_brick_slab'), 'red': ('mangrove_planks', 'mangrove_stairs', 'mangrove_slab'), 'tile': ('bricks', 'brick_stairs', 'brick_slab'), 'dark_wood': ('dark_oak_planks', 'dark_oak_stairs', 'dark_oak_slab'), } WOODS = ('oak', 'spruce', 'dark_oak', 'birch', 'jungle', 'acacia', 'mangrove', 'cherry', 'warped') COLORS = ('white', 'light_gray', 'gray', 'black', 'brown', 'red', 'orange', 'yellow', 'lime', 'green', 'cyan', 'light_blue', 'blue', 'purple', 'magenta', 'pink') FUNCTIONS = ('living', 'kitchen', 'sleeping', 'storage', 'workshop', 'study', 'enchanting', 'brewing', 'trading', 'fishing') DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)} OPPOSITE = {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'} def log(wood): """Resolve the timber role including nether stems.""" return 'stripped_warped_stem' if wood == 'warped' else f'stripped_{wood}_log'
hearthwright/navigation.py
# Conservative voxel navigation with directional stair transitions and supported ladders. from collections import deque from .blocks import passable, floor_support, name, split_state from .materials import DIRECTIONS def walkable(grid, p): """Test actual foot/head cells and the supporting floor.""" x, y, z = p return passable(grid.get(x, y, z)) and passable(grid.get(x, y + 1, z)) and (floor_support(grid.get(x, y - 1, z)) or name(grid.get(x, y, z)) == 'ladder') def neighbors(grid, p): """Yield traversable neighbors, allowing rises only on correctly facing stairs.""" x, y, z = p for direction, (dx, dz) in DIRECTIONS.items(): for dy in (0, 1, -1): q = (x + dx, y + dy, z + dz) if not walkable(grid, q): continue if dy: # Stair facing is the direction from the low end to the high end. high = q if dy == 1 else p block = grid.get(high[0], high[1] - 1, high[2]) expected = direction if dy == 1 else {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'}[direction] if not name(block).endswith('_stairs') or split_state(block)[1].get('facing') != expected: # Leaving a ladder onto a landing is also a legal transition. if name(grid.get(*p)) != 'ladder' and name(grid.get(*q)) != 'ladder': continue # A rising player's head sweeps the higher source cell. if not passable(grid.get(x if dy == 1 else q[0], max(y, q[1]) + 1, z if dy == 1 else q[2])): continue yield q if name(grid.get(*p)) == 'ladder': for dy in (-1, 1): q = x, y + dy, z if name(grid.get(*q)) == 'ladder' and walkable(grid, q): yield q def flood(grid, start, allowed=None, maximum=200000): """Return reachable positions and a predecessor map from actual geometry.""" start = tuple(start) if not walkable(grid, start): return {} parents = {start: None} queue = deque([start]) while queue: p = queue.popleft() for q in neighbors(grid, p): if q in parents or allowed is not None and q not in allowed: continue parents[q] = p queue.append(q) if len(parents) > maximum: raise ValueError('navigation resource limit exceeded') return parents def path_to(parents, target): """Recover one deterministic path from a reachable target.""" p = tuple(target) result = [] while p is not None: result.append(p) p = parents[p] return list(reversed(result))
hearthwright/plan.py
# Resolve relationships, constrained domains, room masks and site reservations. from __future__ import annotations from dataclasses import asdict, dataclass, field, replace import hashlib import json import random from .api import Choice, Design, DesignError, choose, document, domain from .components import SITE_COMPONENTS, BUILTIN_FEATURES from .materials import WALLS, ROOFS, WOODS, COLORS, FUNCTIONS, DIRECTIONS @dataclass class Rect: """Hold inclusive horizontal bounds, independent of schematic coordinates.""" x0: int z0: int x1: int z1: int @property def width(self): return self.x1 - self.x0 + 1 @property def depth(self): return self.z1 - self.z0 + 1 @property def center(self): return ((self.x0 + self.x1) // 2, (self.z0 + self.z1) // 2) def inset(self, n): return Rect(self.x0 + n, self.z0 + n, self.x1 - n, self.z1 - n) def cells(self): return {(x, z) for x in range(self.x0, self.x1 + 1) for z in range(self.z0, self.z1 + 1)} def contains(self, x, z): return self.x0 <= x <= self.x1 and self.z0 <= z <= self.z1 @dataclass class Storey: """Record a resolved floor and its buildable room program.""" id: str rect: Rect floor: int height: int functions: tuple layout: str wall: str attic: bool cells: set = field(default_factory=set) @dataclass class Mass: """Record a connected envelope with a roof and support requirements.""" name: str rect: Rect levels: list[Storey] roof: dict | None envelope: str circulation: str supports: str roots: dict join: dict | None = None @dataclass class Plan: """Retain sampled decisions and spatial relationships for reproduction.""" design: Design seed: int masses: list[Mass] attachments: list[dict] features: list[dict] entrance: dict site: Rect def summary(self): masses = [] for m in self.masses: v = asdict(m) for level in v['levels']: level['cells'] = sorted(level['cells']) masses.append(v) return {'schema': 1, 'seed': self.seed, 'design': document(self.design), 'masses': masses, 'attachments': [{**a, 'rect': asdict(a['rect'])} for a in self.attachments], 'features': [{**a, 'rect': asdict(a['rect'])} for a in self.features], 'entrance': self.entrance, 'site': asdict(self.site)} def signature(self): """Hash geometry and room decisions, excluding labels, materials and planting.""" payload = [] for m in self.masses: payload.append([asdict(m.rect), [(asdict(s.rect), s.floor, s.height, s.layout, s.functions, s.attic) for s in m.levels], m.roof, m.envelope, m.circulation, m.supports, m.roots if m.envelope == 'roots' else None]) payload += [[a['kind'], asdict(a['rect']), a['floor'], a['cover']] for a in self.attachments] payload += [[a['kind'], asdict(a['rect'])] for a in self.features] payload.append([self.entrance['side'], self.entrance['floor'], self.entrance['point']]) return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest() def edge_point(rect, side, bay='center'): """Return a facade point selected by a relative bay.""" fraction = {'left': .3, 'center': .5, 'right': .7}[bay] if side in ('south', 'north'): return (rect.x0 + round((rect.width - 1) * fraction), rect.z1 if side == 'south' else rect.z0) return (rect.x1 if side == 'east' else rect.x0, rect.z0 + round((rect.depth - 1) * fraction)) def outline(rect, organic=False): """Chamfer organic floor corners, retaining ample domestic room area.""" cells = rect.cells() if organic: cells = {(x, z) for x, z in cells if min(x - rect.x0, rect.x1 - x) + min(z - rect.z0, rect.z1 - z) >= 2} return cells def _resolve(design: Design, seed: int, rng) -> Plan: """Compile a declaration into compatible, bounded spatial decisions. Args: design: Public composition. seed: Any signed 64-bit integer. Returns: Plan: Resolved architecture, independent of block placement. """ if type(seed) is not int or not -(2**63) <= seed < 2**63: raise DesignError('seed must be a signed 64-bit integer') if not design.volumes: raise DesignError('a dwelling needs at least one volume') mat = design.materials for value, collection, role in ((mat.wall, WALLS, 'wall'), (mat.roof, ROOFS, 'roof'), (mat.frame, WOODS, 'frame'), (mat.trim, (*WOODS, 'sandstone'), 'trim'), (mat.floor, WOODS, 'floor'), (mat.accent, COLORS, 'accent')): if value not in collection: raise DesignError(f'unknown {role} role {value!r}') if design.site.terrain not in ('meadow', 'slope', 'water', 'hill'): raise DesignError('terrain must be meadow, slope, water or hill') if design.site.terrain == 'hill' and not any(v.envelope == 'roots' for v in design.volumes): raise DesignError('hill terrain needs a root envelope to define its mound; use slope for a framed earth-sheltered building') if not 3 <= design.site.margin <= 12 or not 1 <= design.site.water_depth <= 3 or not 2 <= design.site.rise <= 7: raise DesignError('site margin 3..12, water_depth 1..3, rise 2..7 required') if design.site.paving not in ('garden', 'courtyard', 'yard'): raise DesignError('site paving must be garden, courtyard or yard') if design.site.terrain == 'water' and design.site.paving != 'garden': raise DesignError('water sites require garden paving; use supported deck attachments for hard surfaces') if design.site.planting not in ('temperate', 'mediterranean', 'conifer', 'wetland', 'sparse'): raise DesignError('unknown planting policy') masses, by_name = [], {} for v in design.volumes: if isinstance(v.levels, Choice): selected = choose(v.levels, rng, lambda levels: isinstance(levels, tuple) and 1 <= len(levels) <= (2 if v.envelope == 'roots' else 4), f'{v.name}.levels') v = replace(v, levels=selected) if v.name in by_name: raise DesignError(f'duplicate volume {v.name!r}') if not 1 <= len(v.levels) <= 4: raise DesignError(f'{v.name}: provide 1..4 occupied levels') if v.envelope not in ('framed', 'roots') or v.supports not in ('foundation', 'posts'): raise DesignError(f'{v.name}: unknown envelope or supports') if v.envelope == 'roots' and (v.roof is not None or len(v.levels) > 2): raise DesignError(f'{v.name}: root envelopes need roof=None and at most two levels') if v.envelope != 'roots' and v.roof is None: raise DesignError(f'{v.name}: framed envelopes require a Roof') if not 0 <= v.raised <= 7: raise DesignError(f'{v.name}: raised must be 0..7') if design.site.terrain == 'water' and (v.supports != 'posts' or not v.join and v.raised < design.site.water_depth + 1): raise DesignError(f'{v.name}: water buildings need posts and raised >= water_depth + 1') max_inset = max(s.inset for s in v.levels) min_width = 9 + max_inset * 2 min_depth = 9 + max_inset * 2 stairs_required = len(v.levels) > 1 and domain(v.circulation) == ('stairs',) if stairs_required: min_depth = max(min_depth, max(s.height for s in v.levels[:-1]) + 6 + max_inset * 2) min_width = max(min_width, 11 + max_inset * 2) w = choose(v.width, rng, lambda n: type(n) is int and min_width <= n <= 25, f'{v.name}.width (minimum {min_width}, maximum 25)') d = choose(v.depth, rng, lambda n: type(n) is int and min_depth <= n <= 23, f'{v.name}.depth (minimum {min_depth}, maximum 23)') rect = Rect(0, 0, w - 1, d - 1) floor = 2 + v.raised join = None if v.join: j = v.join if j.to not in by_name: raise DesignError(f'{v.name}: join target must be an earlier volume') parent = by_name[j.to] if not 0 <= j.level < len(parent.levels): raise DesignError(f'{v.name}: join level does not exist') if v.raised: raise DesignError(f'{v.name}: joined floor inherits its target level; omit raised') target = parent.levels[j.level] side = choose(j.side, rng, lambda s: s in DIRECTIONS, f'{v.name}.join.side') align = choose(j.align, rng, lambda s: s in ('front', 'center', 'back'), f'{v.name}.join.align') fraction = {'front': 0, 'center': .5, 'back': 1}[align] p = target.rect inset = v.levels[0].inset if side in ('east', 'west'): z0 = p.z0 + round((p.depth - (d - 2 * inset)) * fraction) - inset x0 = p.x1 - inset if side == 'east' else p.x0 - w + 1 + inset else: x0 = p.x0 + round((p.width - (w - 2 * inset)) * fraction) - inset z0 = p.z1 - inset if side == 'south' else p.z0 - d + 1 + inset rect = Rect(x0, z0, x0 + w - 1, z0 + d - 1) floor = target.floor q = rect.inset(inset) if side in ('east', 'west'): point = (p.x1 if side == 'east' else p.x0, (max(p.z0, q.z0) + min(p.z1, q.z1)) // 2) else: point = ((max(p.x0, q.x0) + min(p.x1, q.x1)) // 2, p.z1 if side == 'south' else p.z0) join = {'to': j.to, 'level': j.level, 'side': side, 'point': point, 'floor': floor} elif masses: raise DesignError(f'{v.name}: every additional volume needs a Join') levels = [] for i, s in enumerate(v.levels): if not 0 <= s.inset <= 3 or s.wall is not None and s.wall not in WALLS: raise DesignError(f'{v.name}/{i}: inset must be 0..3 and wall a material role') if not 5 <= s.height <= 7: raise DesignError(f'{v.name}/{i}: storey height must be 5..7') if s.attic and (i != len(v.levels) - 1 or v.roof is None): raise DesignError(f'{v.name}/{i}: attic must be the last level under a roof') if not s.rooms or any(r not in FUNCTIONS for r in s.rooms): raise DesignError(f'{v.name}/{i}: invalid room function') r = rect.inset(s.inset) options = ('open',) if v.envelope == 'roots' else tuple(['open'] + (['cross'] if r.width >= 15 and len(s.rooms) > 1 else []) + (['long'] if r.depth >= 15 and len(s.rooms) > 1 else [])) layout = choose(s.layout, rng, lambda p: p in options, f'{v.name}/{i}.layout; feasible {options}') height = 3 if s.attic else s.height levels.append(Storey(f'{v.name}/{i}', r, floor, height, s.rooms, layout, s.wall or mat.wall, s.attic, outline(r, v.envelope == 'roots'))) floor += s.height can_stair = all(min(a.rect.depth, b.rect.depth) >= a.height + 6 and min(a.rect.width, b.rect.width) >= 11 for a, b in zip(levels, levels[1:])) circulation = choose(v.circulation, rng, lambda c: c == 'ladder' or c == 'stairs' and can_stair, f'{v.name}.circulation') roof = None if v.roof: r = v.roof form = choose(r.form, rng, lambda t: t in ('gable', 'hip'), f'{v.name}.roof.form') ridge = choose(r.ridge, rng, lambda t: t in ('x', 'z', 'long', 'short'), f'{v.name}.roof.ridge') if ridge in ('long', 'short'): ridge = ('x' if w >= d else 'z') if ridge == 'long' else ('z' if w >= d else 'x') pitch = choose(r.pitch, rng, lambda n: n in (.5, 1.0, 1.5) and (not levels[-1].attic or n >= 1), f'{v.name}.roof.pitch') if r.eaves not in (1, 2): raise DesignError('roof eaves must be 1 or 2') length = levels[-1].rect.width if ridge == 'x' else levels[-1].rect.depth dormers = choose(r.dormers, rng, lambda n: type(n) is int and 0 <= n <= (2 if length >= 17 else 1) and (n == 0 or form == 'gable' and pitch >= 1), f'{v.name}.roof.dormers') cupola = choose(r.cupola, rng, lambda b: type(b) is bool, f'{v.name}.roof.cupola') roof = dict(form=form, ridge=ridge, pitch=pitch, eaves=r.eaves, dormers=dormers, cupola=cupola) roots = dict(spread=choose(v.root_spread, rng, lambda n: type(n) is int and 3 <= n <= 6, 'root_spread'), crown=choose(v.root_crown, rng, lambda n: type(n) is int and 3 <= n <= 7, 'root_crown')) mass = Mass(v.name, rect, levels, roof, v.envelope, circulation, v.supports, roots, join) # Shared boundary is legal; overlapping occupied interiors are not. for other in masses: for a in mass.levels: for b in other.levels: if max(a.floor, b.floor) < min(a.floor + a.height + 1, b.floor + b.height + 1): if a.rect.inset(1).cells() & b.rect.inset(1).cells(): raise DesignError(f'{v.name} intersects occupied space in {other.name}; change join side/alignment or insets') masses.append(mass) by_name[v.name] = mass entry = design.entrance main = by_name.get(entry.volume if entry else masses[0].name) if main is None or entry and not 0 <= entry.level < len(main.levels): raise DesignError('entrance volume/level does not exist') side = entry.side if entry else 'south' if side not in DIRECTIONS: raise DesignError('entrance side must be a cardinal direction') level = main.levels[entry.level if entry else 0] bay = choose(entry.bay if entry else 'center', rng, lambda b: b in ('left', 'center', 'right'), 'entrance.bay') entrance = dict(volume=main.name, level=entry.level if entry else 0, side=side, point=edge_point(level.rect, side, bay), floor=level.floor) attachments = [] occupied = [m.rect.inset(-2) for m in masses] for index, a in enumerate(design.attachments): if a.to not in by_name or not 0 <= a.level < len(by_name[a.to].levels): raise DesignError('attachment target/level does not exist') if a.kind not in ('porch', 'deck', 'terrace', 'balcony', 'market') or a.cover not in ('open', 'pergola', 'canopy'): raise DesignError('invalid attachment kind or cover') s = by_name[a.to].levels[a.level] side_a = choose(a.side, rng, lambda t: t in DIRECTIONS, 'attachment.side') depth = choose(a.depth, rng, lambda n: type(n) is int and 3 <= n <= 7, 'attachment.depth') span = s.rect.width if side_a in ('north', 'south') else s.rect.depth width = choose(a.width, rng, lambda n: n == 'full' or type(n) is int and 5 <= n <= span, 'attachment.width') width = span if width == 'full' else width cx, cz = edge_point(s.rect, side_a) if side_a in ('north', 'south'): x0 = cx - width // 2 z0 = cz + 1 if side_a == 'south' else cz - depth ar = Rect(x0, z0, x0 + width - 1, z0 + depth - 1) else: z0 = cz - width // 2 x0 = cx + 1 if side_a == 'east' else cx - depth ar = Rect(x0, z0, x0 + depth - 1, z0 + width - 1) for m in masses: for l in m.levels: if l.floor <= s.floor < l.floor + l.height and ar.cells() & l.rect.cells(): raise DesignError(f'attachment {index} overlaps {m.name}; use a free facade') for previous in attachments: if abs(previous['floor'] - s.floor) < 4 and ar.cells() & previous['rect'].cells(): raise DesignError(f'attachment {index} overlaps another outdoor floor') attachments.append(dict(id=f'attachment/{index}', kind=a.kind, to=a.to, level=a.level, side=side_a, rect=ar, floor=s.floor, cover=a.cover, door=(cx, cz))) occupied.append(ar) # Lay site plots beside the complete massing, never at reference-specific coordinates. minx = min(r.x0 for r in occupied) maxx = max(r.x1 for r in occupied) minz = min(r.z0 for r in occupied) maxz = max(r.z1 for r in occupied) features = [] cursors = dict(east=minz, west=minz, north=minx, south=minx) for index, f in enumerate(design.features): if f.kind not in (*BUILTIN_FEATURES, *SITE_COMPONENTS): raise DesignError(f'unknown site component {f.kind!r}') side_f = choose(f.side, rng, lambda t: t in DIRECTIONS, 'feature.side') scale_domain = SITE_COMPONENTS[f.kind].scale if f.kind in SITE_COMPONENTS else (5, 11) if f.kind == 'windmill': scale_domain = (9, 11) size = choose(f.scale, rng, lambda n: type(n) is int and scale_domain[0] <= n <= scale_domain[1], f'{f.kind}.scale; feasible {scale_domain[0]}..{scale_domain[1]}') start = cursors[side_f] if side_f in ('east', 'west'): x0 = maxx + 4 if side_f == 'east' else minx - 3 - size z0 = start else: x0 = start z0 = maxz + 4 if side_f == 'south' else minz - 3 - size fr = Rect(x0, z0, x0 + size - 1, z0 + size - 1) for previous in features: if fr.inset(-1).cells() & previous['rect'].cells(): raise DesignError(f'feature {index} overlaps {previous["id"]}; change its side or scale') cursors[side_f] += size + 3 features.append(dict(id=f'feature/{index}', kind=f.kind, side=side_f, rect=fr, version=SITE_COMPONENTS[f.kind].version if f.kind in SITE_COMPONENTS else '1')) occupied.append(fr) # Match the actual terrain at the outer landing, including uphill entrances. dx, dz = DIRECTIONS[side] ex, ez = entrance['point'] extension = max([a['rect'].depth if side in ('north', 'south') else a['rect'].width for a in attachments if a['to'] == entrance['volume'] and a['level'] == entrance['level'] and a['side'] == side] + [0]) first_tread = extension + 1 landing = design.site.water_depth + 1 if design.site.terrain == 'water' else 1 minimum_length = main.roots['spread'] + 3 + (level.rect.x0 - main.rect.x0) if main.envelope == 'roots' else 0 for _ in range(16): length = max(minimum_length, abs(entrance['floor'] - landing) + first_tread + 3) actual = 1 + min(design.site.rise, max(0, masses[0].rect.z1 - (ez + dz * length) + 2) // 2) if design.site.terrain == 'slope' else landing if actual == landing: break landing = actual else: raise DesignError('entrance approach cannot meet this slope; change its wall or level') entrance.update(first_tread=first_tread, landing_floor=landing, approach_length=length) occupied.append(Rect(ex + dx * length - 2, ez + dz * length - 2, ex + dx * length + 2, ez + dz * length + 2)) extra = max([m.roots['spread'] + 3 for m in masses if m.envelope == 'roots'] + [design.site.margin]) site = Rect(min(r.x0 for r in occupied) - extra, min(r.z0 for r in occupied) - extra, max(r.x1 for r in occupied) + extra, max(r.z1 for r in occupied) + extra) if max(site.width, site.depth) + 2 > design.limits.horizontal: raise DesignError('site exceeds horizontal resource limit; reduce components or increase Limits') plan = Plan(design, seed, masses, attachments, features, entrance, site) return plan class _DecisionRandom(random.Random): """Retain finite decisions so later constraints can backtrack within declared domains.""" def __init__(self, seed, tape): super().__init__(seed) self.tape = tape self.visited = [] def pick(self, candidates): if len(candidates) == 1: return candidates[0] first = self.randrange(len(candidates)) index = self.tape[len(self.visited)] if len(self.visited) < len(self.tape) else 0 index = min(index, len(candidates) - 1) self.visited.append((index, len(candidates))) return candidates[(first + index) % len(candidates)] def resolve(design: Design, seed: int, *, max_candidates=128) -> Plan: """Search bounded declared choices, retaining explicit decisions without fallback presets. Args: design: Architectural composition, with concrete values or bounded domains. seed: Signed 64-bit integer controlling all choice ordering. max_candidates: Maximum complete or partial plans examined before a diagnostic. Returns: Plan: Feasible resolved architecture with no geometric mutations to user intent. """ if type(seed) is not int or not -(2**63) <= seed < 2**63: raise DesignError('seed must be a signed 64-bit integer') tape = [] error = None for attempt in range(max_candidates): rng = _DecisionRandom(seed, tape) try: plan = _resolve(design, seed, rng) return plan except DesignError as exc: error = exc visited = rng.visited for i in range(len(visited) - 1, -1, -1): branch, count = visited[i] if branch + 1 < count: tape = [v[0] for v in visited[:i]] + [branch + 1] break else: raise DesignError(f'contradictory specification: {exc}') from exc raise DesignError(f'planning budget ({max_candidates} candidates) exhausted: {error}; narrow the conflicting domains')
hearthwright/roofs.py
# Backed roof unions, dormer height fields and structural root sweeps. import math from .materials import ROOFS, WALLS, DIRECTIONS, log from .blocks import is_air def surface(mass): """Resolve a gable or hip to a sealed height field with integrated dormer bumps.""" s = mass.levels[-1] r = mass.roof rect = s.rect.inset(-r['eaves']) base = s.floor + s.height result = {} dormer_centers = [] if r['dormers']: lo, hi = (s.rect.x0, s.rect.x1) if r['ridge'] == 'x' else (s.rect.z0, s.rect.z1) dormer_centers = [lo + round((hi - lo) * f) for f in ([.5] if r['dormers'] == 1 else [.27, .73])] for x, z in sorted(rect.cells()): dx = min(x - rect.x0, rect.x1 - x) dz = min(z - rect.z0, rect.z1 - z) cross = dz if r['ridge'] == 'x' else dx run = min(dx, dz) if r['form'] == 'hip' else cross h = base + math.floor(run * r['pitch']) face = ('south' if z < rect.center[1] else 'north') if r['ridge'] == 'x' else ('east' if x < rect.center[0] else 'west') verge = (x in (rect.x0, rect.x1)) if r['ridge'] == 'x' else (z in (rect.z0, rect.z1)) if r['form'] == 'hip' and dx < dz: face = 'east' if x < rect.center[0] else 'west' if r['form'] == 'hip' and dz <= dx: face = 'south' if z < rect.center[1] else 'north' for center in dormer_centers: lateral = x if r['ridge'] == 'x' else z along = z if r['ridge'] == 'x' else x front = s.rect.z1 if r['ridge'] == 'x' else s.rect.x0 into = front - along if r['ridge'] == 'x' else along - front if abs(lateral - center) <= 2 and -1 <= into <= 4: dh = base + 5 - abs(lateral - center) if dh > h: h = dh face = ('east' if x < center else 'west') if r['ridge'] == 'x' else ('south' if z < center else 'north') verge = into == -1 result[x, z] = dict(height=h, face=face, trim=verge, owner=mass.name) return result, dormer_centers def build_roofs(ctx): """Merge roof surfaces before placement, then close gables and install crowns.""" g, plan, mat = ctx.g, ctx.plan, ctx.plan.design.materials maps, centers = {}, {} merged = {} for mass in plan.masses: if mass.roof is None: continue maps[mass.name], centers[mass.name] = surface(mass) for p, value in maps[mass.name].items(): if p not in merged or value['height'] > merged[p]['height']: merged[p] = value for mass in plan.masses: if mass.roof is None: continue s = mass.levels[-1] heights = maps[mass.name] inner = ctx.inner[s.id] for x, z in sorted(s.cells): h = heights[x, z]['height'] if (x, z) not in inner: for y in range(s.floor + s.height, h): g.put(x, y, z, log(mat.frame) if x == s.rect.center[0] or z == s.rect.center[1] else g.rng.choice(WALLS[s.wall]), **({'axis': 'y'} if x == s.rect.center[0] or z == s.rect.center[1] else {})) elif s.attic: g.box((x, s.floor + 1, z, x, h - 1, z), 'air') ctx.interior.update((x, y, z) for y in range(s.floor + 1, h)) # Dormer glazing sits in the actual continuous gable wall. for center in centers[mass.name]: if mass.roof['ridge'] == 'x': points = [(center + d, s.floor + s.height + y, s.rect.z1) for d in (-1, 1) for y in (1, 2)] side = 'south' else: points = [(s.rect.x0, s.floor + s.height + y, center + d) for d in (-1, 1) for y in (1, 2)] side = 'west' for p in points: g.put(*p, 'light_gray_stained_glass') g.metadata['openings'].append({'kind': 'window', 'owner': s.id, 'cells': points, 'side': side}) backing, stair, slab = ROOFS[mat.roof] trim = ('smooth_sandstone', 'smooth_sandstone_stairs', 'smooth_sandstone_slab') if mat.trim == 'sandstone' else (mat.trim + '_planks', mat.trim + '_stairs', mat.trim + '_slab') for (x, z), v in sorted(merged.items()): h = v['height'] # Eaves stop against taller inhabited volumes, sealing at their solid walls. if any(m.name != v['owner'] and any((x, z) in ctx.inner[s.id] and s.floor <= h + 1 <= s.floor + s.height for s in m.levels) for m in plan.masses): continue b, st, sl = trim if v['trim'] else (backing, stair, slab) neighbor = [merged[x + dx, z + dz]['height'] for dx, dz in DIRECTIONS.values() if (x + dx, z + dz) in merged] covered = [surface_map[x, z]['height'] for surface_map in maps.values() if (x, z) in surface_map] low = min(neighbor + covered, default=h) owner = next(m for m in plan.masses if m.name == v['owner']) if (x, z) in owner.levels[-1].cells: low = max(low, owner.levels[-1].floor + owner.levels[-1].height) # Solid vertical risers eliminate cracks even for the 3:2 profile. for y in range(low, h + 1): g.put(x, y, z, b) g.put(x, h, z, b) # Ridge caps use slabs. Other courses preserve a clear tile rhythm. if all(h >= v2 for v2 in neighbor) and sum(h == v2 for v2 in neighbor) >= 1: g.put(x, h + 1, z, sl, type='bottom') else: g.put(x, h + 1, z, st, facing=v['face'], half='bottom', shape='straight') ctx.roof_maps = maps for mass in plan.masses: if not mass.roof: continue s, roof = mass.levels[-1], mass.roof if roof['form'] == 'gable': # Paired upper gable lights fit under the actual backed slope. axis_x = roof['ridge'] == 'x' for end in (s.rect.x0, s.rect.x1) if axis_x else (s.rect.z0, s.rect.z1): for offset in (-2, 2): x, z = (end, s.rect.center[1] + offset) if axis_x else (s.rect.center[0] + offset, end) top = maps[mass.name][x, z]['height'] - 2 bottom = max(s.floor + s.height + 1, top - 2) if top >= bottom: for y in range(bottom, top + 1): g.put(x, y, z, 'light_gray_stained_glass') # Corbels attach the thick eaves to the top wall beam. for x, z in sorted(s.cells - ctx.inner[s.id]): if (x + z) % 4: continue for side, (dx, dz) in DIRECTIONS.items(): if (x + dx, z + dz) in s.cells: continue y = s.floor + s.height - 1 if is_air(g.get(x + dx, y, z + dz)): g.stair(x + dx, y, z + dz, mat.floor, {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'}[side], 'top') for mass in plan.masses: if mass.roof and mass.roof['cupola']: cupola(ctx, mass, maps[mass.name]) def cupola(ctx, mass, heights): """Build a sealed ridge lantern with an explicit non-occupied solid core.""" g, mat = ctx.g, ctx.plan.design.materials cx, cz = mass.levels[-1].rect.center base = heights[cx, cz]['height'] + 1 roofmat, stairs, slab = ROOFS[mat.roof] for x in range(cx - 2, cx + 3): for z in range(cz - 2, cz + 3): bottom = heights[x, z]['height'] g.box((x, bottom, z, x, base, z), log(mat.frame) if x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) else 'calcite', **({'axis': 'y'} if x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) else {})) for y in range(base + 1, base + 4): edge = x in (cx - 2, cx + 2) or z in (cz - 2, cz + 2) corner = x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) g.put(x, y, z, log(mat.frame) if corner else 'glass' if edge else 'glowstone', **({'axis': 'y'} if corner else {})) for radius in (3, 2, 1, 0): y = base + 7 - radius for x in range(cx - radius, cx + radius + 1): for z in range(cz - radius, cz + radius + 1): g.put(x, y, z, roofmat) if radius and (x in (cx - radius, cx + radius) or z in (cz - radius, cz + radius)): face = 'east' if x == cx - radius else 'west' if x == cx + radius else 'south' if z == cz - radius else 'north' g.put(x, y + 1, z, stairs, facing=face) g.put(cx, base + 8, cz, 'lightning_rod', facing='up') def sweep_root(g, points, radius=1): """Sweep connected oriented timber through an arbitrary polyline.""" for a, b in zip(points, points[1:]): delta = [b[i] - a[i] for i in range(3)] length = max(abs(d) for d in delta) axis = 'xyz'[max(range(3), key=lambda i: abs(delta[i]))] for step in range(length + 1): center = [round(a[i] + delta[i] * step / max(1, length)) for i in range(3)] for dx in range(-radius, radius + 1): for dy in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dy) + abs(dz) <= radius * 2: p = center[0] + dx, max(1, center[1] + dy), center[2] + dz g.put(*p, 'stripped_dark_oak_log' if dx == 0 else 'dark_oak_log', axis=axis) def root_envelope(ctx, mass): """Grow a mound, broad hollowable trunk and radial buttresses before carving rooms.""" g = ctx.g r = mass.rect cx, cz = r.center spread = mass.roots['spread'] crown = mass.roots['crown'] top = mass.levels[-1].floor + mass.levels[-1].height + 2 rx, rz = r.width / 2 + spread, r.depth / 2 + spread for x in range(math.floor(cx - rx), math.ceil(cx + rx) + 1): for z in range(math.floor(cz - rz), math.ceil(cz + rz) + 1): radial = ((x - cx) / rx)**2 + ((z - cz) / rz)**2 if radial >= 1: continue h = 1 + round((top - 1) * (1 - radial)**.5) for y in range(2, h + 1): block = 'moss_block' if y == h else g.rng.choice(('tuff', 'andesite', 'mossy_cobblestone', 'dirt')) g.put(x, y, z, block) # The trunk is a tapered irregular solid, not a box with logs on its corners. for y in range(top - 4, top + crown): radius = max(2, 4 - max(0, y - top) // 2) for x in range(cx - radius, cx + radius + 1): for z in range(cz - radius, cz + radius + 1): if abs(x - cx) + abs(z - cz) <= radius + 1: g.put(x, y, z, 'dark_oak_log', axis='y') count = 7 + (spread % 3) for i in range(count): angle = (i + .15) * math.tau / count dx, dz = math.cos(angle), math.sin(angle) # Skip the entrance sector; its two flanking roots remain wide apart. if dz > .8 and abs(dx) < .35: continue tipx, tipz = round(cx + dx * (rx - 1)), round(cz + dz * (rz - 1)) points = [(cx + round(dx * 2), top + crown - 2, cz + round(dz * 2)), (cx + round(dx * (r.width / 2 - 1)), top - 2, cz + round(dz * (r.depth / 2 - 1))), (tipx, 3, tipz), (tipx + round(dx), 2, tipz + round(dz))] sweep_root(g, points) for sign, lift in ((-1, crown), (1, crown - 1)): sweep_root(g, [(cx + sign, top, cz), (cx + sign * 3, top + lift, cz), (cx + sign * 4, top + lift + 2, cz), (cx + sign * 2, top + lift + 2, cz)], 1) # Carving and the closed irregular room boundary are shared with ordinary envelopes.
hearthwright/site.py
# Terrain, outdoor rooms, approaches and adaptable property features. from .components import SITE_COMPONENTS from .blocks import is_air, name, full_support from .materials import DIRECTIONS, OPPOSITE, log from .plan import edge_point def terrain(ctx): """Create bounded land or shallow water before foundations and excavation.""" g, p = ctx.g, ctx.plan site = p.design.site main = p.masses[0].rect for x, z in sorted(p.site.cells()): # Rounded cut corners keep the diorama edge quiet. if min(x - p.site.x0, p.site.x1 - x) + min(z - p.site.z0, p.site.z1 - z) < 2: continue h = 1 if site.terrain == 'slope': h += min(site.rise, max(0, main.z1 - z + 2) // 2) g.put(x, 0, z, g.rng.choice(('stone', 'stone', 'andesite'))) if site.terrain == 'water': edge = min(x - p.site.x0, p.site.x1 - x, z - p.site.z0, p.site.z1 - z) if edge < 2: h = site.water_depth g.box((x, 1, z, x, h - 1, z), 'dirt') g.put(x, h, z, 'grass_block') else: g.put(x, 0, z, g.rng.choice(('mud', 'clay', 'gravel'))) g.box((x, 1, z, x, site.water_depth, z), 'water', level='0') h = 0 else: g.box((x, 1, z, x, h - 1, z), 'dirt') finish = 'grass_block' if site.paving != 'garden' and p.site.inset(4).contains(x, z): finish = ('terracotta' if x % 5 < 2 and z % 5 < 2 else 'smooth_sandstone') if site.paving == 'courtyard' else g.rng.choice(('coarse_dirt', 'coarse_dirt', 'gravel')) g.put(x, h, z, finish) ctx.ground[x, z] = h def column(ctx, x, z, top, owner, timber=True): """Take a post or pier to actual ground or waterbed, recording its load path.""" g, mat = ctx.g, ctx.plan.design.materials bottom = ctx.ground.get((x, z), 0) block = log(mat.frame) if timber else 'stone_bricks' g.box((x, bottom, z, x, top, z), block, **({'axis': 'y'} if timber else {})) g.metadata['supports'].append({'owner': owner, 'bottom': (x, bottom, z), 'top': (x, top, z), 'kind': 'post' if timber else 'pier'}) def attachment(ctx, a): """Construct supported outdoor floors, open pergolas or striped market canopies.""" g, mat = ctx.g, ctx.plan.design.materials r, y = a['rect'], a['floor'] block = ('smooth_sandstone' if mat.wall == 'white' else 'stone_bricks') if a['kind'] == 'terrace' else mat.floor + '_planks' for x, z in sorted(r.cells()): g.put(x, y, z, block if (x + z) % 5 else (('terracotta' if mat.wall == 'white' else 'andesite') if a['kind'] == 'terrace' else log(mat.floor))) for x in (r.x0, r.x1): for z in (r.z0, r.z1): column(ctx, x, z, y, a['id'], a['kind'] != 'terrace') side = a['side'] dx, dz = DIRECTIONS[side] cx, cz = a['door'] # Keep a generous center gateway in the rail and an approach to the door. outer = edge_point(r, side) for x, z in sorted(r.cells()): if x not in (r.x0, r.x1) and z not in (r.z0, r.z1): continue on_wall = (side == 'south' and z == r.z0 or side == 'north' and z == r.z1 or side == 'east' and x == r.x0 or side == 'west' and x == r.x1) gate = abs((x - outer[0]) if dz else (z - outer[1])) <= 1 if not on_wall and not gate: g.put(x, y + 1, z, mat.frame + '_fence') g.door(cx, y, cz, side, a['id'], 'warped' if mat.wall == 'white' else 'spruce') ctx.opening_route((cx, y + 1, cz), side, a['id']) centerline = [] for step in range(1, max(r.width, r.depth) + 1): x, z = cx + dx * step, cz + dz * step if not r.contains(x, z): break g.box((x, y + 1, z, x, y + 2, z), 'air') centerline.append((x, y + 1, z)) g.route(centerline, a['id']) a['target'] = centerline[len(centerline) // 2] if a['cover'] != 'open': beam = y + 5 for x in (r.x0, r.x1): for z in (r.z0, r.z1): g.box((x, y + 1, z, x, beam, z), log(mat.frame), axis='y') for z in (r.z0, r.z1): g.box((r.x0, beam, z, r.x1, beam, z), log(mat.frame), axis='x') for x in (r.x0, r.x1): g.box((x, beam, r.z0, x, beam, r.z1), log(mat.frame), axis='z') if a['cover'] == 'pergola': for z in range(r.z0, r.z1 + 1, 2): g.box((r.x0, beam + 1, z, r.x1, beam + 1, z), mat.floor + '_slab', type='bottom') else: for x, z in sorted(r.cells()): g.put(x, beam, z, 'red_wool' if (x - r.x0) % 4 < 2 else 'white_wool') g.lantern(r.x0 + 1, beam - 1, r.z0, a['id'], True) else: g.put(r.x0, y + 1, r.z0, mat.frame + '_fence') g.lantern(r.x0, y + 2, r.z0, a['id']) # Domestic furniture at the edge, separated from the reserved entry axis. fx, fz = r.x0 + 1, r.z0 + 1 if (fx, y + 1, fz) not in g.reservations: g.put(fx, y + 1, fz, 'barrel' if a['kind'] == 'market' else mat.floor + '_stairs', **({'facing': 'south'})) if a['kind'] == 'market': g.put(fx, y + 2, fz, 'melon') g.inventory((fx, y + 1, fz), [('emerald', 8), ('bread', 12)]) g.metadata['features'].append({'owner': a['id'], 'kind': a['kind'], 'target': a['target']}) def approach(ctx): """Connect a dry start to the threshold with supported directional stairs.""" g, p, mat = ctx.g, ctx.plan, ctx.plan.design.materials e = p.entrance x, z = e['point'] floor, side = e['floor'], e['side'] dx, dz = DIRECTIONS[side] first_tread = e['first_tread'] ground_floor = e['landing_floor'] length = e['approach_length'] points = [] for d in range(1, length + 1): px, pz = x + dx * d, z + dz * d uphill = ground_floor > floor if uphill: y = min(ground_floor, floor + max(0, d - first_tread)) stepped = first_tread < d <= first_tread + ground_floor - floor else: y = max(ground_floor, floor - max(0, d - first_tread)) stepped = d >= first_tread and y > ground_floor for w in (-1, 0, 1): xx, zz = px + (w if dz else 0), pz + (w if dx else 0) material = mat.floor + '_planks' if p.design.site.terrain == 'water' else 'stone_bricks' g.box((xx, 0, zz, xx, y, zz), material) if stepped: facing = side if uphill else OPPOSITE[side] g.stair(xx, y, zz, mat.floor if p.design.site.terrain == 'water' else 'stone_brick', facing) g.metadata['stairs'].append({'owner': 'approach', 'point': (xx, y, zz), 'facing': facing}) # Clear the corridor through terrain and root buttresses before decoration. g.box((xx, y + 1, zz, xx, max(y + (3 if d > first_tread else 2), ctx.ground.get((xx, zz), 1) + 2), zz), 'air') points.append((px, y + 1, pz)) g.route(points, 'approach') ctx.start = points[-1] g.metadata['start'] = ctx.start g.metadata['entrance'] = (x, floor + 1, z) def approach_lighting(ctx): """Place public path lamps after path construction, outside all reserved passages.""" g, p = ctx.g, ctx.plan sx, sy, sz = ctx.start candidates = [(sx + dx, sz + dz) for dx in range(-6, 7) for dz in range(-6, 7) if 2 <= abs(dx) + abs(dz) <= 6] candidates.sort(key=lambda q: (abs(q[0] - sx) + abs(q[1] - sz), q)) installed = [] for x, z in candidates: if not p.site.inset(1).contains(x, z) or any(abs(x - px) + abs(z - pz) < 4 for px, pz in installed): continue y = p.design.site.water_depth + 1 if p.design.site.terrain == 'water' else ctx.ground.get((x, z), 1) if abs(y + 2 - sy) > 4 or not g.free([(x, y + 1, z), (x, y + 2, z)]): continue if p.design.site.terrain == 'water': column(ctx, x, z, y, 'approach-light') elif not full_support(g.get(x, y, z)): continue g.put(x, y + 1, z, 'stone_brick_wall') g.lantern(x, y + 2, z, 'approach') installed.append((x, z)) if len(installed) == 2: return if not installed: raise ValueError('no clear site edge for an approach lamp') def feature(ctx, f): """Realize a reserved feature plot with contained water, supports and access.""" g, p, mat = ctx.g, ctx.plan, ctx.plan.design.materials r, kind = f['rect'], f['kind'] if kind in SITE_COMPONENTS: SITE_COMPONENTS[kind].realize(ctx, f) return cx, cz = r.center # All working plots get an engineered level platform on their terrain. y = max(ctx.ground.get(q, 1) for q in r.cells()) if p.design.site.terrain == 'water': y = p.design.site.water_depth + 1 elif kind == 'landing': # A river reach is excavated around the jetty, with a continuous gravel bed. y = 2 for x, z in sorted(r.inset(-2).cells()): g.box((x, 1, z, x, max(3, ctx.ground.get((x, z), 1) + 2), z), 'air') g.put(x, 0, z, 'gravel') g.put(x, 1, z, 'water', level='0') ctx.ground[x, z] = 0 if kind in ('landing',): block = mat.floor + '_planks' else: block = 'grass_block' for x, z in sorted(r.cells()): if kind != 'landing': g.box((x, 1, z, x, y - 1, z), 'dirt') g.put(x, y, z, block) if kind == 'pool': for x, z in sorted(r.cells()): edge = x in (r.x0, r.x1) or z in (r.z0, r.z1) g.put(x, y, z, 'smooth_quartz' if edge else 'water', **({} if edge else {'level': '0'})) if not edge: g.put(x, y - 1, z, 'prismarine_bricks') # Two loungers occupy the dry coping edge. for x in (r.x0 + 1, r.x1 - 1): g.stair(x, y + 1, r.z1, 'birch', 'north') elif kind == 'field': for x, z in sorted(r.inset(1).cells()): if x == cx: g.put(x, y, z, 'water', level='0') else: g.put(x, y, z, 'farmland', moisture='7') g.put(x, y + 1, z, 'wheat', age=str(g.rng.choice((5, 7, 7, 7)))) for x, z in sorted(r.cells() - r.inset(1).cells()): if (x, z) != (cx, r.z1): g.put(x, y + 1, z, 'stone_brick_slab', type='bottom') elif kind == 'well': g.box((cx - 1, y, cz - 1, cx + 1, y + 1, cz + 1), 'stone_bricks') g.put(cx, y + 1, cz, 'water', level='0') for x in (cx - 2, cx + 2): for z in (cz - 2, cz + 2): g.box((x, y + 1, z, x, y + 5, z), log(mat.frame), axis='y') for x in range(cx - 3, cx + 4): for z in range(cz - 3, cz + 4): g.put(x, y + 6, z, mat.floor + '_slab', type='bottom') g.box((cx - 2, y + 4, cz, cx + 2, y + 4, cz), log(mat.frame), axis='x') g.put(cx, y + 3, cz, 'chain', axis='y') elif kind == 'windmill': g.box((cx - 1, y + 1, cz - 1, cx + 1, y + 5, cz + 1), 'stone_bricks') g.box((cx, y + 6, cz, cx, y + 11, cz), log(mat.frame), axis='y') g.box((cx - 1, y + 11, cz - 1, cx + 1, y + 11, cz + 1), mat.floor + '_slab', type='bottom') sy, sz = y + 8, cz + 2 g.put(cx, sy, cz + 1, log(mat.frame), axis='z') for step in range(-4, 5): g.put(cx + step, sy, sz, log(mat.frame), axis='x') g.put(cx, sy + step, sz, log(mat.frame), axis='y') for d in (2, 3, 4): for width in (1,): for xx, yy in ((cx + width, sy + d), (cx + d, sy - width), (cx - width, sy - d), (cx - d, sy + width)): g.put(xx, yy, sz, 'oak_trapdoor', facing='south', open='true') g.put(cx + 1, y + 1, cz + 2, 'grindstone', face='floor', facing='south') elif kind == 'landing': for x in (r.x0, r.x1): for z in (r.z0, r.z1): column(ctx, x, z, y + 1, f['id']) g.put(r.x1, y + 2, r.z1, 'lantern', hanging='false') g.put(r.x0 + 1, y + 1, r.z0 + 1, 'barrel') g.inventory((r.x0 + 1, y + 1, r.z0 + 1), [('oak_boat', 1), ('fishing_rod', 1), ('lead', 2)]) # A low mooring arm has visible water beside the bollard. for x in range(r.x1 + 1, r.x1 + 3): g.put(x, y, r.z1, mat.floor + '_planks') column(ctx, r.x1 + 2, r.z1, y + 1, f['id']) else: for x, z in sorted(r.inset(1).cells()): if (x + z) % 2 == 0: g.put(x, y + 1, z, g.rng.choice(('allium', 'cornflower', 'oxeye_daisy'))) f['target'] = (cx, y + 1, r.z1 + 1) f['floor'] = y g.metadata['features'].append({'owner': f['id'], 'kind': kind, 'target': f['target']}) # Lit working edge outside the entrance into the feature. g.put(r.x0, y + 1, r.z1, 'stone_bricks') g.lantern(r.x0, y + 2, r.z1, f['id']) def connect_features(ctx): """Route exterior walks around occupied footprints using a bounded grid search.""" from collections import deque g, p = ctx.g, ctx.plan start = ctx.start[0], ctx.start[2] forbidden = set() for mass in p.masses: forbidden |= mass.rect.inset(-2 if mass.envelope == 'framed' else -mass.roots['spread']).cells() for a in p.attachments: forbidden |= a['rect'].cells() for f in p.features: forbidden |= f['rect'].cells() for route in g.metadata['routes']: forbidden |= {(x, z) for x, _, z in route['points']} forbidden |= {(s['point'][0], s['point'][2]) for s in g.metadata['stairs']} forbidden.discard(start) targets = [(f['target'], f['id']) for f in p.features] # Secondary upper terraces connect through their own doors; they need no ground stair. for target, owner in targets: target2 = target[0], target[2] forbidden.discard(target2) parents = {start: None} queue = deque([start]) while queue and target2 not in parents: x, z = queue.popleft() for dx, dz in DIRECTIONS.values(): q = x + dx, z + dz if q not in parents and q not in forbidden and p.site.contains(*q): parents[q] = (x, z) queue.append(q) if target2 not in parents: raise ValueError(f'{owner}: no clear exterior approach; change its side') path = [] q = target2 while q is not None: path.append(q) q = parents[q] path.reverse() # Paths use the entrance landing elevation. Terraced rises use a dedicated last run. start_y = ctx.start[1] - 1 target_y = target[1] - 1 difference = target_y - start_y if abs(difference) > len(path) - 2: raise ValueError(f'{owner}: path too short for terrain transition') points = [] for i, (x, z) in enumerate(path): h = start_y remaining = len(path) - 1 - i if remaining < abs(difference): h += (1 if difference > 0 else -1) * (abs(difference) - remaining) g.box((x, 0, z, x, h - 1, z), 'stone_bricks' if p.design.site.terrain != 'water' else 'spruce_planks') g.put(x, h, z, 'coarse_dirt' if p.design.site.terrain != 'water' else 'spruce_planks') g.box((x, h + 1, z, x, max(h + 3, ctx.ground.get((x, z), 1) + 2), z), 'air') if difference > 0 and remaining < difference: px, pz = path[i - 1] facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - px, z - pz)) g.stair(x, h, z, 'stone_brick', facing) elif difference < 0 and remaining <= -difference and i + 1 < len(path): nx, nz = path[i + 1] facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - nx, z - nz)) g.stair(x, h, z, 'stone_brick', facing) points.append((x, h + 1, z)) g.route(points, owner) def planting(ctx): """Add restrained gardens and trees only in genuinely free site margins.""" g, p = ctx.g, ctx.plan policy = p.design.site.planting forbidden = set() for m in p.masses: forbidden |= m.rect.inset(-3).cells() for a in p.attachments + p.features: forbidden |= a['rect'].inset(-2).cells() for route in g.metadata['routes']: for x, _, z in route['points']: forbidden |= {(x + dx, z + dz) for dx in range(-2, 3) for dz in range(-2, 3)} for light in g.metadata['lights']: x, _, z = light['point'] forbidden |= {(x + dx, z + dz) for dx in range(-2, 3) for dz in range(-2, 3)} candidates = [(x, z) for x, z in sorted(p.site.inset(3).cells()) if (x, z) not in forbidden and name(g.get(x, ctx.ground.get((x, z), 0), z)) == 'grass_block'] tree_count = 0 if policy == 'sparse' else 2 g.rng.shuffle(candidates) trunks = [] for x, z in candidates: if len(trunks) >= tree_count: break if any(abs(x - tx) + abs(z - tz) < 10 for tx, tz in trunks): continue if any((x + dx, z + dz) in forbidden for dx in range(-2, 3) for dz in range(-2, 3)): continue tree(ctx, x, z, policy) trunks.append((x, z)) for x, z in sorted(p.site.inset(1).cells()): if (x, z) in forbidden: continue y = ctx.ground.get((x, z), 0) + 1 if not is_air(g.get(x, y, z)): continue if name(g.get(x, y - 1, z)) == 'grass_block' and g.rng.random() < .12: g.put(x, y, z, g.rng.choice(('short_grass', 'short_grass', 'fern', 'cornflower', 'oxeye_daisy'))) elif p.design.site.terrain == 'water' and name(g.get(x, p.design.site.water_depth, z)) == 'water' and g.rng.random() < .035: g.put(x, p.design.site.water_depth + 1, z, 'lily_pad') def tree(ctx, x, z, policy): """Grow a small connected tree using the site's planting vocabulary.""" g = ctx.g base = ctx.ground[x, z] height = g.rng.randint(9, 12) if policy == 'conifer' else g.rng.randint(7, 10) if policy == 'mediterranean' else g.rng.randint(5, 8) wood = 'jungle' if policy == 'mediterranean' else 'spruce' if policy == 'conifer' else 'oak' g.box((x, base + 1, z, x, base + height, z), wood + '_log', axis='y') leaves = set() if policy == 'mediterranean': for dx, dz in DIRECTIONS.values(): for d in range(4): leaves.add((x + dx * d, base + height + (1 if d < 3 else 0), z + dz * d)) if d == 3: leaves.add((x + dx * d, base + height - 1, z + dz * d)) elif policy == 'conifer': for level in range(3, height + 2): radius = max(0, (height - level) // 4 + (1 if level % 3 == 0 else 0)) for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dz) <= radius + 1: leaves.add((x + dx, base + level, z + dz)) else: for dy in range(-3, 2): radius = max(0, 2 - abs(dy) // 2) if policy != 'conifer' else (2 if dy % 2 == 0 else 1) for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dz) <= radius + 1: leaves.add((x + dx, base + height + dy, z + dz)) for point in sorted(leaves): if is_air(g.get(*point)): g.put(*point, wood + '_leaves', persistent='true', distance='1')
hearthwright/validate.py
# Reusable diagnostics over final or reloaded geometry and explicit design metadata. from __future__ import annotations from collections import deque from dataclasses import dataclass, field, asdict from pathlib import Path from .components import SITE_COMPONENTS, BUILTIN_FEATURES from .blocks import Grid, AIR, registry, split_state, name, is_air, passable, seal, full_support, floor_support, blockstates_equivalent from .materials import DIRECTIONS from .navigation import flood, walkable @dataclass(frozen=True) class Diagnostic: """Identify a failed rule, responsible component and optional world coordinate.""" rule: str component: str message: str location: tuple | None = None @dataclass class Report: """Collect reusable validation results without raising until explicitly requested.""" diagnostics: list[Diagnostic] = field(default_factory=list) checked: dict = field(default_factory=dict) @property def valid(self): return not self.diagnostics def add(self, rule, component, message, location=None): self.diagnostics.append(Diagnostic(rule, component, message, tuple(location) if location is not None else None)) def require_valid(self): if not self.valid: details = '\n'.join(f'{d.rule}: {d.component} {d.location or ""}: {d.message}' for d in self.diagnostics[:25]) raise ValidationError(f'{len(self.diagnostics)} architectural validation failures\n{details}', self) return self def to_dict(self): return {'valid': self.valid, 'checked': self.checked, 'diagnostics': [asdict(d) for d in self.diagnostics]} class ValidationError(ValueError): """Carry the full report for programmatic error handling.""" def __init__(self, message, report): super().__init__(message) self.report = report RULES = [] def validation_rule(function): """Register an additional pure geometry rule receiving grid, metadata and report.""" if function not in RULES: RULES.append(function) return function def validate(building, metadata=None) -> Report: """Validate an artifact, sparse grid or reloaded path with explicit design metadata. Args: building: Building, Grid, or Path to a Litematic with a sidecar. metadata: Optional metadata dictionary or sidecar Path for reloaded geometry. Returns: Report: Diagnostics with component and coordinate information. """ if isinstance(building, (str, Path)): g = Grid.load(Path(building), metadata) else: g = building if isinstance(building, Grid) else building.grid m = g.metadata if metadata is None or isinstance(metadata, Path) else metadata report = Report() if m.get('schema') != 1 or any(k not in m for k in ('rooms', 'routes', 'start', 'interior')): report.add('metadata.schema', 'artifact', 'schema 1 architecture metadata with rooms, routes, start and interior is required') return report for rule in (states_and_bounds, multiblocks, attachments, circulation, envelope, furnishing, *RULES): rule(g, m, report) for kind in {f['kind'] for f in m.get('features', [])}: if kind in SITE_COMPONENTS and SITE_COMPONENTS[kind].rule: SITE_COMPONENTS[kind].rule(g, m, report) for feature in m.get('resolved', {}).get('features', []): kind = feature['kind'] if kind not in BUILTIN_FEATURES: if kind not in SITE_COMPONENTS: report.add('component.missing', kind, 'register the recorded site component to run its validation rules') elif feature.get('version') != SITE_COMPONENTS[kind].version: report.add('component.version', kind, 'registered component version differs from the exported design') report.checked['blocks'] = len(g.blocks) return report def states_and_bounds(g, m, report): """Validate every explicit property and derived or recorded bounds.""" domains = registry() for state in sorted(set(g.blocks.values())): try: n, props = split_state(state) short = n.removeprefix('minecraft:') if short not in domains: raise ValueError('unknown block') if any(k not in domains[short][0] or v not in domains[short][0][k] for k, v in props.items()): raise ValueError('unknown property or value') except (TypeError, ValueError) as exc: point = next(p for p, s in g.blocks.items() if s == state) report.add('block.state', 'geometry', f'{state}: {exc}', point) if 'size_xyz' in m: ox, oy, oz = m['origin'] sx, sy, sz = m['size_xyz'] for p in g.blocks: if not (ox <= p[0] < ox + sx and oy <= p[1] < oy + sy and oz <= p[2] < oz + sz): report.add('bounds.export', 'geometry', 'block outside recorded schematic bounds', p) break b = g.bounds size = [b[i + 3] - b[i] + 3 for i in range(3)] limits = m.get('limits', {}) if limits and (max(size[0], size[2]) > limits['horizontal'] or size[1] > limits['vertical'] or size[0] * size[1] * size[2] > limits['volume'] or len(g.blocks) > limits['blocks']): report.add('bounds.resources', 'geometry', f'geometry size {size} exceeds configured Limits') report.checked['block_states'] = len(set(g.blocks.values())) def multiblocks(g, m, report): """Check both halves of doors and beds, as well as their actual support.""" count = 0 for (x, y, z), state in g.blocks.items(): n = name(state) if n.endswith('_door'): _, props = split_state(state) dy = 1 if props.get('half') == 'lower' else -1 other = g.get(x, y + dy, z) on, op = split_state(other) expected = dict(props, half='upper' if dy == 1 else 'lower') if on != 'minecraft:' + n or op != expected: report.add('multiblock.door', 'door', 'matching opposite door half is missing or inconsistent', (x, y, z)) if dy == 1 and not floor_support(g.get(x, y - 1, z)): report.add('support.door', 'door', 'door has no solid threshold', (x, y, z)) count += 1 elif n.endswith('_bed'): _, props = split_state(state) dx, dz = DIRECTIONS.get(props.get('facing'), (0, 0)) sign = 1 if props.get('part') == 'foot' else -1 other = g.get(x + sign * dx, y, z + sign * dz) on, op = split_state(other) if on != 'minecraft:' + n or op != dict(props, part='head' if sign == 1 else 'foot'): report.add('multiblock.bed', 'bed', 'adjacent matching bed head/foot is missing', (x, y, z)) if not full_support(g.get(x, y - 1, z)): report.add('support.bed', 'bed', 'bed is unsupported', (x, y, z)) count += 1 report.checked['multiblock_cells'] = count def attachments(g, m, report): """Check continuous supports, ladder backing, lantern suspension and plant substrate.""" for support in m['supports']: x, y0, z = support['bottom'] _, y1, _ = support['top'] for y in range(y0, y1 + 1): if not full_support(g.get(x, y, z)): report.add('support.column', support['owner'], 'load path to ground or waterbed is interrupted', (x, y, z)) break for (x, y, z), state in g.blocks.items(): n = name(state) if n == 'ladder': facing = split_state(state)[1].get('facing') dx, dz = DIRECTIONS.get(facing, (0, 0)) if not full_support(g.get(x - dx, y, z - dz)): report.add('support.ladder', 'ladder', 'rung lacks a solid backing wall', (x, y, z)) elif n == 'lantern': hanging = split_state(state)[1].get('hanging') == 'true' other = g.get(x, y + (1 if hanging else -1), z) if not full_support(other) and not name(other).endswith(('_fence', '_wall', '_slab')) and name(other) not in ('chain', 'lantern'): report.add('support.light', 'lantern', 'light has no supporting bracket or ceiling', (x, y, z)) elif n in ('poppy', 'cornflower', 'allium', 'oxeye_daisy', 'short_grass', 'fern', 'dandelion'): if name(g.get(x, y - 1, z)) not in ('grass_block', 'dirt', 'rooted_dirt', 'moss_block', 'podzol', 'coarse_dirt'): report.add('support.plant', 'planting', 'invalid substrate', (x, y, z)) elif n == 'wheat' and name(g.get(x, y - 1, z)) != 'farmland': report.add('support.crop', 'field', 'wheat needs farmland', (x, y, z)) elif n == 'lily_pad' and name(g.get(x, y - 1, z)) != 'water': report.add('support.lily', 'water', 'lily pad needs water', (x, y, z)) report.checked['support_columns'] = len(m['supports']) def circulation(g, m, report): """Inspect final clearance and traverse actual doors, stairs and ladders.""" resolved = m.get('resolved', {}) if resolved.get('design', {}).get('site', {}).get('terrain') != 'water': dx, dz = DIRECTIONS[resolved.get('entrance', {}).get('side', 'south')] x, y, z = m['start'] outside = (x + dx, y, z + dz) if not walkable(g, outside): report.add('reachability.terrain', 'approach', 'outer landing does not meet clear, walkable terrain', outside) for route in m['routes']: for x, y, z in route['points']: if any(not passable(g.get(x, y + dy, z)) for dy in range(route['headroom'])): report.add('clearance.route', route['owner'], 'reserved route has less than two clear headroom blocks', (x, y, z)) break if not walkable(g, (x, y, z)): report.add('support.route', route['owner'], 'route lacks a usable floor or ladder', (x, y, z)) break for stair in m['stairs']: state = g.get(*stair['point']) if not name(state).endswith('_stairs') or split_state(state)[1].get('facing') != stair['facing']: report.add('circulation.stair', stair['owner'], 'stair tread missing or faces against the ascent', stair['point']) x, y, z = stair['point'] if not full_support(g.get(x, y - 1, z)): report.add('support.stair', stair['owner'], 'tread lacks solid support', stair['point']) try: reachable = flood(g, m['start']) except ValueError as exc: report.add('circulation.limit', 'artifact', str(exc)) reachable = {} for room in m['rooms']: if tuple(room['anchor']) not in reachable: report.add('reachability.room', room['id'], 'room is unreachable from the exterior start', room['anchor']) for x, z in room['cells']: p = (x, room['floor'] + 1, z) if walkable(g, p) and p not in reachable: report.add('reachability.floor', room['id'], 'usable floor area is isolated from the entrance', p) break if tuple(m['entrance']) not in reachable: report.add('reachability.entrance', 'entrance', 'the actual approach does not reach the door', m['entrance']) for f in m['features']: if tuple(f['target']) not in reachable: report.add('reachability.site', f['owner'], 'outdoor working space is unreachable', f['target']) for f in m['furniture']: if tuple(f['access']) not in reachable: report.add('reachability.furniture', f['owner'], f'{f["kind"]} interaction space is blocked', f['access']) report.checked['reachable_positions'] = len(reachable) report.checked['rooms'] = len(m['rooms']) def envelope(g, m, report): """Flood interior voids to detect leaks, including later damage to roofs and walls.""" interior = {tuple(p) for p in m['interior']} visited = set() for room in m['rooms']: start = tuple(room['anchor']) if start in visited or seal(g.get(*start)): continue queue = deque([start]) visited.add(start) failure = None while queue and failure is None: x, y, z = queue.popleft() for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)): q = x + dx, y + dy, z + dz if q in visited or seal(g.get(*q)): continue if q not in interior: failure = q break visited.add(q) queue.append(q) if failure is not None: report.add('envelope.leak', room['id'], 'interior air reaches outside its weather envelope', failure) for window in m['openings']: if window['kind'] != 'window': continue side = window['side'] dx, dz = DIRECTIONS[side] for x, y, z in window['cells']: if 'glass' not in name(g.get(x, y, z)): report.add('opening.glazing', window['owner'], 'glazed aperture was overwritten', (x, y, z)) break # A room-side full block obstructs light and view; roof cavities are exempt. q = x - dx, y, z - dz if q in interior and not passable(g.get(*q)): report.add('opening.clearance', window['owner'], 'window reveal is blocked inside the room', q) break for room in m['rooms']: for x, z in room['cells']: if name(g.get(x, room['floor'] + 1, z)) in ('water', 'dirt', 'grass_block', 'rooted_dirt', 'mud'): report.add('envelope.intrusion', room['id'], 'water or hillside material invades the usable room', (x, room['floor'] + 1, z)) break report.checked['envelope_voids'] = len(visited) def furnishing(g, m, report): """Require realized furniture, functional coverage and nearby light for every room.""" for room in m['rooms']: furnished = {f['function'] for f in m['furniture'] if f['owner'] == room['id']} for function in room['functions']: if function not in furnished: report.add('program.missing', room['id'], f'no realized furniture for {function}') floor = room['floor'] actual_lights = [record for record in m['lights'] if record['owner'] == room['id'] and name(g.get(*record['point'])) in ('lantern', 'glowstone', 'sea_lantern') and floor < record['point'][1] <= floor + 4] if not actual_lights: report.add('lighting.room', room['id'], 'room lacks an actual light near its occupied floor') for feature in m['features']: if not any(light['owner'] == feature['owner'] and name(g.get(*light['point'])) in ('lantern', 'glowstone', 'sea_lantern') for light in m['lights']): report.add('lighting.site', feature['owner'], 'important outdoor space lacks its actual light', feature['target']) for owner, target in (('entrance', m['entrance']), ('approach', m['start'])): nearby = [light for light in m['lights'] if name(g.get(*light['point'])) == 'lantern' and sum(abs(a - b) for a, b in zip(light['point'], target)) <= 10 and abs(light['point'][1] - target[1]) <= 4] if not nearby: report.add('lighting.approach', owner, 'public approach needs an actual nearby light', target) for f in m['furniture']: for p, expected in zip(f['cells'], f['states']): if not blockstates_equivalent(g.get(*p), expected): report.add('furnishing.changed', f['owner'], f'{f["kind"]} was removed or overwritten', p) break for record in m.get('inventories', []): p = tuple(record['point']) expected = [tuple(item) for item in record['items']] actual = [tuple(item) for item in g.inventories.get(p, [])] if expected != actual: report.add('inventory.changed', 'container', 'actual block entity inventory differs from the declared supplies', p) report.checked['furniture_groups'] = len(m['furniture'])
examples/__init__.py
# Independent, declarative client compositions; no geometry or validation implementations.
examples/canal_apothecary.py
# A new composition: piled plaster quarters, a brewing wing and an open waterside pergola. from hearthwright import Attachment, Choice, Design, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Canal apothecary', volumes=( Volume('home', width=Choice(13, 15), depth=13, raised=3, supports='posts', levels=(Level(('living', 'kitchen', 'sleeping')),), roof=Roof(form='hip', pitch=.5)), Volume('laboratory', width=9, depth=Choice(9, 11), join=Join('home', 'east', Choice('front', 'back')), supports='posts', levels=(Level(('brewing', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='white', frame='birch', roof='tile', trim='sandstone', accent='cyan'), site=Site(terrain='water', planting='wetland'), attachments=(Attachment('deck', 'home', cover='pergola', depth=4),), features=(Feature('landing', 'west', 5),), ) if __name__ == '__main__': run(DESIGN)
examples/courtyard_house.py
# A new composition: three connected masses frame a sheltered entrance court. from hearthwright import Attachment, Choice, Design, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Three gables court', volumes=( Volume('hall', width=Choice(23, 25), depth=Choice(11, 13), levels=(Level( ('living', 'kitchen', 'study')), Level(('sleeping', 'storage'), layout='cross')), roof=Roof(ridge='x', pitch=.5)), Volume('work', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'front'), levels=(Level(('workshop', 'storage')),), roof=Roof(ridge='z', pitch=1.0)), Volume('guest', width=9, depth=Choice(11, 13), join=Join('hall', 'south', 'back'), levels=(Level(('sleeping', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='plaster', frame='spruce', roof='slate', accent='blue'), site=Site(margin=3, planting='sparse', paving='yard'), attachments=(Attachment('porch', 'hall', width=5, depth=3, cover='pergola'),), ) if __name__ == '__main__': run(DESIGN)
examples/hillside_studio.py
# A new composition: hollow roots joined to a glazed masonry workshop wing. from hearthwright import Choice, Design, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Elderroot studio', volumes=( Volume('hollow', width=Choice(13, 15), depth=Choice(11, 13), envelope='roots', roof=None, root_crown=Choice(3, 5), levels=(Level(('living', 'kitchen', 'sleeping')),)), Volume('studio', width=9, depth=Choice(9, 11), join=Join('hollow', Choice('east', 'west'), 'center'), levels=(Level(('workshop', 'study')),), roof=Roof(pitch=.5)), ), materials=Materials(wall='earth', frame='dark_oak', roof='dark_wood', accent='green'), site=Site(terrain='hill', planting='sparse'), ) if __name__ == '__main__': run(DESIGN)
examples/mediterranean_villa.py
# Offset plaster volumes, turquoise shutters, open terraces and a garden pool. from hearthwright import Attachment, Choice, Design, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'House of the blue courtyard', volumes=( Volume('house', width=Choice(15, 17), depth=Choice(13, 15), levels=(Level(('living', 'kitchen')), Level(('sleeping', 'brewing'), layout=Choice('open', 'cross'))), roof=Roof(ridge='x', pitch=.5)), Volume('library', width=Choice(9, 11), depth=9, join=Join('house', 'east', Choice('front', 'back')), levels=(Level(('enchanting', 'study')),), roof=Roof(pitch=.5, ridge='z')), ), materials=Materials(wall='white', frame='birch', roof='tile', trim='sandstone', accent='cyan'), site=Site(planting='mediterranean', paving='courtyard', margin=3), attachments=(Attachment('terrace', 'house', depth=4, cover='pergola'), Attachment('balcony', 'house', level=1, side='west', width=7, cover='pergola')), features=(Feature('pool', 'south', Choice(7, 9)), Feature('garden', 'west', 5)), ) if __name__ == '__main__': run(DESIGN)
examples/merchant_manor.py
# Stone trading undercroft, upper entrance, dark gables and a connected study tower. from hearthwright import Attachment, Choice, Design, Entrance, Join, Level, Materials, Roof, Volume from hearthwright.cli import run DESIGN = Design( 'Rowan merchants house', volumes=( Volume('hall', width=Choice(17, 19), depth=Choice(15, 17), levels=(Level(('storage', 'trading'), wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross')), Level(('sleeping', 'study'), attic=True)), roof=Roof(pitch=1.0, ridge='x', dormers=Choice(1, 2))), Volume('tower', width=9, depth=9, join=Join('hall', 'north', Choice('front', 'back'), level=1), levels=(Level(('study',)), Level(('storage',)), Level(('study',))), circulation='ladder', roof=Roof(pitch=1.5, ridge='z')), ), materials=Materials(wall='plaster', frame='spruce', roof='slate', trim='spruce', accent='red'), entrance=Entrance('hall', level=1, bay=Choice('center', 'right')), attachments=(Attachment('terrace', 'hall', level=1, depth=3), Attachment('market', 'hall', side='east', width=7, depth=5, cover='canopy')), ) if __name__ == '__main__': run(DESIGN)
examples/orchard_house.py
# A new design space with a sampled floor count, domestic loft and covered working terrace. from hearthwright import Attachment, Choice, Design, Feature, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Orchard keeper house', volumes=(Volume( 'home', width=Choice(13, 15), depth=Choice(13, 15), levels=Choice((Level(('living', 'kitchen', 'sleeping', 'storage')),), (Level( ('living', 'kitchen', 'workshop')), Level(('sleeping', 'storage'), attic=True))), roof=Roof(form=Choice('gable', 'hip'), ridge=Choice('x', 'z'), pitch=1.0)),), materials=Materials(wall='plaster', frame='dark_oak', roof='sandstone', trim='dark_oak', accent='green'), site=Site(planting='temperate', margin=4), attachments=(Attachment('terrace', 'home', side='east', width=7, cover='pergola'),), features=(Feature('field', 'west', Choice(7, 9)), Feature('well', 'south', 7)), ) if __name__ == '__main__': run(DESIGN)
examples/red_roof_farmstead.py
# Red tiled farmhouse with a usable loft and a working farmyard. from hearthwright import Choice, Design, Feature, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Red ridge farm', volumes=(Volume('farmhouse', width=Choice(15, 17), depth=Choice(13, 15), levels=(Level(('living', 'kitchen', 'storage')), Level(('sleeping', 'workshop'), layout=Choice('open', 'cross'), attic=True)), roof=Roof(pitch=Choice(1.0, 1.5), ridge='x', dormers=1, cupola=True)),), materials=Materials(wall='timber', frame='spruce', roof='red', trim='sandstone', accent='red'), site=Site(margin=4, planting='sparse'), features=(Feature('well', 'west', 7), Feature('field', 'east', Choice(7, 9)), Feature('windmill', 'east', 9)), ) if __name__ == '__main__': run(DESIGN)
examples/root_house.py
# Inhabited organic mound, hollow polygonal rooms and spreading dark roots. from hearthwright import Choice, Design, Level, Materials, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Hollow elder home', volumes=(Volume('hollow', width=Choice(13, 15), depth=Choice(13, 15), levels=(Level(('living', 'kitchen', 'storage'), wall='earth'), Level(('sleeping', 'study'), wall='earth', inset=1)), envelope='roots', roof=None, circulation=Choice('stairs', 'ladder'), root_spread=Choice(4, 5, 6), root_crown=Choice(4, 6)),), materials=Materials(wall='earth', frame='dark_oak', floor='spruce', accent='green'), site=Site(terrain='hill', margin=5, planting='temperate'), ) if __name__ == '__main__': run(DESIGN)
examples/sandstone_cottage.py
# Partly buried workshop, broad pale roof, entrance bay and waterside jetty. from hearthwright import Choice, Design, Entrance, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Willowbank house', volumes=( Volume('house', width=Choice(17, 19), depth=Choice(13, 15), levels=(Level(('workshop', 'storage'), wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross'))), roof=Roof(ridge='x', pitch=.5)), Volume('bay', width=9, depth=9, join=Join('house', 'west', Choice('front', 'back'), level=1), levels=(Level(('sleeping', 'study')),), roof=Roof(ridge='z', pitch=1.0)), ), materials=Materials(wall='plaster', frame='spruce', roof='sandstone', trim='dark_oak', accent='green'), entrance=Entrance('house', level=1, bay=Choice('center', 'right')), site=Site(terrain='slope', rise=5, planting='temperate'), features=(Feature('landing', 'south', 5),), ) if __name__ == '__main__': run(DESIGN)
examples/stilt_water_cabin.py
# Compact fishing quarters carried on piles over shallow water. from hearthwright import Attachment, Choice, Design, Feature, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Reedwater fishing cabin', volumes=(Volume('cabin', width=Choice(11, 13), depth=Choice(9, 11), raised=4, supports='posts', levels=(Level(('living', 'sleeping', 'fishing', 'kitchen')),), roof=Roof(ridge=Choice('x', 'z'), pitch=.5, eaves=2)),), materials=Materials(wall='timber', frame='oak', roof='dark_wood', trim='spruce', accent='green'), site=Site(terrain='water', water_depth=2, planting='wetland', margin=4), attachments=(Attachment('deck', 'cabin', side='south', depth=Choice(4, 5)),), features=(Feature('landing', 'west', 5),), ) if __name__ == '__main__': run(DESIGN)
examples/stilted_manor.py
# Recessed masonry base carrying an overhanging timber house and steep inhabited roof. from hearthwright import Attachment, Choice, Design, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Highbeam manor', volumes=(Volume('manor', width=Choice(19, 21), depth=Choice(17, 19), levels=(Level(('workshop', 'storage'), inset=2, wall='masonry'), Level(('living', 'kitchen'), layout=Choice('open', 'cross')), Level(('sleeping', 'study'), layout=Choice('open', 'cross'), attic=True)), roof=Roof(ridge='x', pitch=Choice(1.0, 1.5), dormers=2, cupola=True)),), materials=Materials(wall='plaster', frame='spruce', roof='gray', trim='spruce', accent='red'), site=Site(planting='conifer', margin=5), attachments=(Attachment('balcony', 'manor', level=1, side='east', width=7, depth=3),), ) if __name__ == '__main__': run(DESIGN)
examples/stone_cottage.py
# A compact stone dwelling with a small ladder loft and a sheltered entry. from hearthwright import Choice, Design, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Alder cottage', volumes=(Volume('cottage', width=Choice(9, 11), depth=Choice(9, 11), levels=(Level( ('living', 'kitchen', 'storage')), Level(('sleeping', 'study'), attic=True)), circulation='ladder', roof=Roof(ridge=Choice('x', 'z'), pitch=1.0)),), site=Site(margin=3, planting='sparse'), materials=Materials(wall='stone', roof='pale_stone', frame='oak', trim='spruce'), ) if __name__ == '__main__': run(DESIGN)
examples/woodland_archive.py
# A new composition: a small house, a tall archive wing, hipped roof and working garden. from hearthwright import Attachment, Choice, Design, Feature, Join, Level, Materials, Roof, Site, Volume from hearthwright.cli import run DESIGN = Design( 'Woodland archive', volumes=( Volume('home', width=Choice(11, 13), depth=Choice(11, 13), levels=(Level(('living', 'kitchen', 'sleeping')),), roof=Roof(form=Choice('hip', 'gable'), ridge='x')), Volume('archive', width=9, depth=9, join=Join('home', Choice('east', 'west'), Choice('front', 'back')), levels=(Level(('study', 'enchanting')), Level(('study', 'storage'))), circulation='ladder', roof=Roof(pitch=1.5, ridge='z', cupola=False)), ), materials=Materials(wall='stone', frame='dark_oak', roof='red', trim='dark_oak', accent='blue'), site=Site(planting='temperate'), attachments=(Attachment('porch', 'home', width=7, depth=3, cover='pergola'),), features=(Feature('garden', 'south', Choice(5, 7)),), ) if __name__ == '__main__': run(DESIGN)

12 · 浏览器执行与实测

本轮页面更新新增 分布探索客户端:采样器从原始声明提案,并调用原 library 生成。最多尝试 16 个候选,耗尽时显示诊断;成功后回填独立 Python 声明。此功能由报告阶段添加,不归因给原实验 agent。新增检查覆盖 54 个 seed、全部 13 个起点;另有 13 次浏览器探索、控件回填和独立客户端重放检查。探索验证 · 浏览器记录。结构控件作用于新声明,导出的客户端不依赖网页采样器。

Pyodide 0.27.7 + Web Worker 执行全部 13 个原始 Hearthwright 模块,并加载实验镜像的 MCIO 与 Deprecated 源码。每份运行时文件在加载时检查 SHA-256。生成、验证、MCIO 导出与读回走原库;bridge 负责运行客户端、组织预览方块和下载包。

浏览器 NumPy 2.0.2、tqdm 4.66.2、wrapt 1.16.0 来自该 Pyodide 版本,与容器依赖版本不同,因此下面单独验证兼容性。Pyodide 包加载文档 · 运行时源码哈希 · 浏览器桥接源码。

浏览器实测结果
13 客户端 × 2 seed26/26 浏览器生成、完整几何验证与 MCIO 读回验证通过
与存档的结构对比26/26 规划签名及几何签名相同
与存档的逐格对比9/26 完全相同;17/26 仅吊灯位置不同,共 50 个坐标
容器 NBT26/26 完全相同,包含库存物品
结构控件与 Python 编辑宽度控件改变结构;新的双体量客户端成功生成
确定性同一浏览器、相同输入重复生成的 schematic 字节相同
Seed 边界signed 64-bit 两端可运行,超出范围被拒绝;下载 JSON 保留精度
异常与停止无效屋顶组合返回诊断;停止无限循环后能恢复生成
下载ZIP 包含建筑、验证 JSON 和可运行客户端
桌面与手机1440px 与 375px 已查看;手机无横向溢出;剖切不改导出
本机 Python 总耗时26 份样本约 0.24–1.54 秒/份,不含首次加载与 3D 网格
JavaScript 错误交互检查期间未捕获 pageerror;渲染器有已知 builtin/entity 模型警告

完整实测记录

跨平台细节差异:26 份浏览器结果的规划与几何签名、容器 block entity NBT 均与存档一致。9 份逐格一致,17 份只在吊灯位置有差异,共涉及 50 个方块坐标(移走与新增分别计数);元数据差异仅为 lights。不能声称浏览器完整建筑与存档逐格相同。

对应代码在 furnish.py 中对一个坐标集合按到房间锚点的距离排序,未给等距候选设置坐标作为第二排序键。浏览器 Python 的 hash width 是 32 位,容器是 64 位;这为候选次序变化提供了具体原因。建议将排序键改成距离加坐标元组,再分别验证两种平台。本报告保留原实验代码,未静默修正这一问题。逐样本对比结果。

Seed 以字符串跨越 JavaScript 边界,避免 64 位整数精度损失。停止按钮终止 Worker;失败后保留上一份成功预览。编辑器内容只在点击执行时运行。页面依赖首次下载运行时和本站 Minecraft 材质资源;不是离线安装包。

13 · 下载、证据与复现

归档:harness report · usage · commands · 832 次审计 · 测试输出 · 报告统计。

在原实验环境中运行以下命令。离开容器后需安装 MCIO / NumPy 等依赖,并按 GENERATOR.md 设置 MCIO_ROOT 与 MINECRAFT_STATES。网页生成包里的客户端同样依赖该 library。

# Original container workspace python generate.py --seed 73 --output samples/my-building.litematic python -m examples.courtyard_house --seed 1 --output samples/court.litematic python -m pytest -q python -m scripts.audit --count 64 python -m scripts.sample_batch python -m scripts.render_batch --all-seeds

报告日期:2026-09-25。静态报告及在线执行器遵循 Claude html-report / BrowneUI。保留实验产物,未修改原始生成程序。