Experiment / Procedural generation / 2026.09.24

从八栋参考到一个生成器


让 Codex 写一个能造不同 lodge 的程序,再让这个程序在你的浏览器里运行。

同一 gpt-6-astra · xhigh 会话,八份参考代码与图片,一次关机中断后恢复。 目标是扩大符合参考风格的有效建筑组合,优先结构和布局,不计算参数组合总量。

默认 seed 的完整 lodge,石塔、深色屋顶和林间花园
实验原始产物 · default seed 20260924 · agent 的正面渲染。
139 / 139harness 重跑测试通过
11交付建筑样本
5主体平面拓扑
40 / 40有限种子集的结构签名不同
跳转到 section

01 · 关键观察

  1. 已实现:生成器直接规划体量、房间连接、屋顶和地形。运行时不导入八份参考脚本,也不是八选一派发。
  2. 有限证据:40 个 seed 得到 40 个不同的几何签名,11 个展示样本也各不相同。签名关注屋顶、地板和室内障碍,弱化材质噪声;不同哈希不等于人眼认定的 40 个建筑类型。
  3. 质量边界:139 项测试验证方块、通行、支撑与围护结构。它们不评分美学,也不是 Minecraft 真实碰撞引擎。所有可能参数组合并未穷举。
  4. 统计修正:约 906 万 token 是完整会话的累计输入与输出;大部分是缓存输入。恢复后的 1,297 秒不是整场实验总耗时。
  5. 审阅判断:原始源码已有清晰的 plan / geometry / validation 分工;几何主体仍集中在一个大模块。扩展性有基础,但还需要通过新增建筑规则来检验。

02 · Live proc-gen:自己改一个 lodge

输入 seed 或锁定建筑参数,再点击生成。未锁定项由 seed 采样;同 seed、同参数可复现。 初次需要下载 Python 运行时与材质资源,后续复用。这里运行的是归档的 Python 核心,计算发生在浏览器。

更多建筑参数
默认建筑的静态预览

点击「生成建筑」启动 Python。
当前为实验原始截图。

95

准备就绪。调参数后点击生成;改变控件不会自动启动计算。

本次实际参数与结构签名

结构签名:尚未生成

参数之间有约束。例如一层主楼不能带上层阳台,双侧翼不能同时使用 wrap 门廊。 不合法组合会明确报错。检查失败也会显示错误,不会静默换成别栋建筑;上一份成功预览会保留。

03 · 实验设置与八份参考

模型 gpt-6-astra,effort xhigh,CLI Codex 0.156.1。 运行条件为 proc-lodge × standard,容器限制 4 CPU / 8 GiB 内存,未设运行时限。 MCIO 与 MCRender 库栈只读,agent 修改可写工作区;结束后由独立 harness 用户重跑测试并评分。

./run_agent.sh --prompt proc-lodge --toolset standard \ --cli codex --model gpt-6-astra --effort xhigh

每个 ref 各一份最终生成器,共八份,原始图片一并提供。任务要求程序化组合和可扩展性,不以代码长短为目标。 完整英文 prompt · 参考来源与 SHA-256

ref-1 原始建筑参考
ref-1 · 代码
ref-2 原始建筑参考
ref-2 · 代码
ref-3 原始建筑参考
ref-3 · 代码
ref-4 原始建筑参考
ref-4 · 代码
ref-5 原始建筑参考
ref-5 · 代码
ref-6 原始建筑参考
ref-6 · 代码
ref-7 原始建筑参考
ref-7 · 代码
ref-8 原始建筑参考
ref-8 · 代码
读取本次完整 prompt
# Task: Procedural Lodge Generator Write a procedural generator for furnished Minecraft Java 1.21.1 lodges. Given a seed, it must generate a complete, attractive, logically sound lodge drawn from the architectural distribution represented by ref-1 through ref-8. Maximize the variety of buildings the program can generate while keeping every sample within that distribution and meeting the quality requirements below. The deliverable is a reusable generator with a large design space, together with generated examples and validation. Running it without arguments must produce `output.litematic`. ## Eight references Exactly one generator from each reference run is supplied: | Reference | Source code | Original reference image | | --- | --- | --- | | ref-1 | `references/ref-1/generate.py` | `reference_ref-1.png` | | ref-2 | `references/ref-2/generate.py` | `reference_ref-2.png` | | ref-3 | `references/ref-3/generate.py` | `reference_ref-3.png` | | ref-4 | `references/ref-4/generate.py` | `reference_ref-4.png` | | ref-5 | `references/ref-5/generate.py` | `reference_ref-5.png` | | ref-6 | `references/ref-6/generate.py` | `reference_ref-6.png` | | ref-7 | `references/ref-7/generate.py` | `reference_ref-7.png` | | ref-8 | `references/ref-8/generate.py` | `reference_ref-8.png` | `references/README.md` records their provenance. Inspect all eight generators and images before choosing your procedural design. They define a family, including compact woodland cottages, taller stacked lodges, cross-gabled wings, tower compositions, cliff or terrace settings, inns, and a conservatory extension. Learn their shared architectural language and their differences: massing, proportions, roof silhouettes, timber and plaster framing, stone foundations, room organization, approaches, planting and warm lighting. Keep the scale and detail comparable to the references. Hidden sides and interiors must be complete. Use the supplied code as implementation reference. You may reuse and adapt useful geometry routines, but build a coherent parameterized system that can compose new designs. Selecting one of eight fixed scripts, or repainting a fixed floor plan, does not meet the objective. Each reference should inform reachable parts of your design space, without requiring every lodge to contain every reference feature. 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. ## Objective: maximize valid architectural variety The quantity to maximize is the number of meaningfully different, valid building types expressible by the program's 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, conservatories, terraces and other compatible extensions. - 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. Seeded sampling must exercise these structural choices, not leave them fixed while randomizing texture. Avoid overwhelmingly sampling a single topology or requiring lucky seeds to see major branches. Examples should include new coherent combinations beyond replicas of the eight references. ## Program design Do not optimize for code length or worry about how many files or lines the implementation needs. Write as much code as the result requires. 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: seeded design choices -> an explicit building plan -> structural geometry -> Minecraft blocks -> validation and export. Keep architectural decisions separate from block placement. - 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 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. ## Generator contract - `python generate.py` writes 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. - Expose architectural parameters through a documented Python API or configuration interface. Record each sample's seed and resolved parameters so its design choices can be inspected and reproduced. - 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. The built-in sampler must produce valid configurations. - 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 lodge for most seeds. - Reused implementation should live in the delivered generator modules. Running the finished program 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 lodge must satisfy them. Architecture and aesthetics - Preserve the references' recognizable lodge language, balanced proportions and composed silhouettes. Frame, infill, foundation and roof 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 lodge. Layout must respond to the sampled building. 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. Terrain, terraces, supports and foundations must meet without floating joins. - 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 lodge. Choose a finite, diverse seed corpus and targeted parameter cases that cover every major architectural branch, domain boundaries, and difficult feature 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 determinism, bounds, valid states, support and multi-block consistency, room and level reachability, headroom, envelope integrity and clear openings. - 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. Distinct seeds or file hashes alone do not prove architectural variety. Report only observed sample diversity, never a speculative total number of possible buildings. - Render a representative spread of structurally different seeds 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`: - `generate.py` and any supporting modules: the procedural generator, rerunnable from a clean directory. The standard toolset seeds a writable `generate.py` with placement, export and reload helpers. - `output.litematic`: the default-seed lodge. This exact path remains the harness output contract. - `GENERATOR.md`: concise usage, parameter domains, dependencies, sampling rules and design rationale. Explain how all eight references inform the design space, how structural variation combines, and how constraints preserve quality. Include validation commands and known limitations. Do not compute the total combination count. - `samples/`: a finite, representative collection of generated lodges, with seeds and resolved parameters recorded in a manifest. Provide a rerunnable command or script for the sample batch. - `test/test_*.py`: passing tests covering the generator and multiple architectural configurations. - `previews/`: labeled exterior renders and cutaways for representative samples. Spend the budget on the procedural system, 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 · 关机、恢复与统计口径

原容器在关机后已消失,但宿主机上的工作区与会话 JSONL 保留。 恢复时复制并校验已落盘文件,在新容器中执行 codex exec resume,会话 ID 未变。 这是会话和文件恢复,不是进程内存快照;关机瞬间未保存的请求状态无法恢复。

阶段UTC 时间范围请求token时间含义
中断前2026-09-24T21:43:04
2026-09-24T22:10:23
363,924,517日志跨度,非精确运行时长
恢复后2026-09-25T01:17:10
2026-09-25T01:38:43
345,138,856harness 测量 1,297 秒

同一会话 01a0d55f-3f64-70b2-9843-9b34e19a467b。最终 JSONL 的前 332 条记录与中断前副本逐条相同,统计使用最终完整记录一次,不把两份会话相加。中间关机停顿不算计算耗时。

harness 的 agent.seconds = 1297 仅测量恢复后的进程。 中断前可观察的日志跨度约 27 分 19 秒,真实停止时刻缺少记录,不能给出精确的全程活跃时间。

05 · Token 与运行结果

70完整会话请求
9,063,373累计 total tokens
92.5%输入 token 缓存命中比例
75 / 7shell 命令 / 非零退出
输入 token,按阶段拆分缓存与非缓存。纵轴单位为 token。
输出 token,reasoning 是输出的一部分,不再重复累加。纵轴单位为 token。
指标实测值解释
输入8,979,475含 8,307,072 缓存输入;非缓存输入 672,403
输出83,898含 34,517 reasoning;其余输出 49,381
默认建筑46 × 48 × 5816,787 个非空气方块,110 个 palette states,62 种非空气方块
完成状态agent 0 / score 0环境检查、schematic 可加载检查及 pytest 通过

数据来自每次请求的 token_usage_record.usage,不是上下文窗口大小,不是独立新信息量,也不是账单金额。没有可靠单价和账单核对,因此不估算费用。

06 · 程序怎么组织

flowchart LR
  A["Seed + overrides"] --> B["Plan.resolve"]
  B --> C["Scene.build"]
  C --> D["Geometry validation"]
  D --> E["MCIO export"]
  D --> F["Browser preview"]
      
拖动平移 · 滚轮缩放
原生成器的规划、几何与验证模块直接用于浏览器。原 CLI 使用 MCIO;浏览器使用单独的导出适配层。
模块职责边界
lodge/plan.py参数依赖、体量、连接接口、原点和边界先决定可用结构,再放方块
lodge/geometry.py地形、主体、屋顶、楼梯、家具、种植预留通道和开口,控制后续放置
lodge/validation.py对最终方块做通行、支撑、门床和外壳检查不只信任计划中的连接关系
generate.py / blocks.py公开 API、CLI、导出和读回比较语义比较忽略属性顺序,仍检测实际差异

当前文法以正交矩形体量组合,支持 single / ell / twin / courtyard / staggered。 主楼直跑楼梯、侧翼与塔楼梯子、固定六格层高是实际实现边界;没有曲墙、螺旋楼梯或悬崖折返步道。

07 · 参数空间与多样性

设计目标是扩大有效的建筑差异,不报告组合乘积。取值之间存在依赖,无翼楼时 wing 参数也不改变建筑。 保留字段不代表增加了一种类型。

参数含义取值
topology平面拓扑["single", "ell", "twin", "courtyard", "staggered"]
width主体宽度[15, 17, 19]
depth主体深度[17, 19, 21]
floors主楼完整层数[1, 2, 3]
ridge屋脊方向["z", "x"]
profile屋顶轮廓["chalet", "steep", "bell"]
wing_size侧翼宽度[9, 11, 13]
wing_floors侧翼层数[1, 2]
wing_ridge侧翼屋脊["parallel", "cross"]
placement侧翼连接位置["front", "middle", "rear"]
tower塔楼类型["none", "stone", "timber"]
tower_extra塔楼加层[1, 2]
tower_width塔楼宽度[9, 11]
conservatory玻璃温室[false, true]
porch门廊["entry", "full", "wrap"]
balcony阳台[false, true]
dormers老虎窗[0, 1, 2]
terrain地形["garden", "terrace", "cliff"]
approach入口步道["straight", "landing"]
entrance门的位置["center", "left", "right"]
layout室内布局["open", "cross", "suites"]
stair_side楼梯侧["left", "right"]
palette材质家族["honey", "ivory", "ochre"]
trees树木数量[3, 4, 5]

40 个结构签名全部不同,只证明这个有限样本集在定义的几何特征上不同。 它不能证明所有 seed 唯一、所有组合合法,或这些结果在美学上同样出色。建筑种类需要结合体量、布局与渲染人工审阅。

08 · 11 份实际样本

这些文件由实验 agent 交付,网页没有替它修改建筑。表格可排序、筛选;点击在线生成会用对应输入重新运行 Python。

样本seed拓扑层 / 塔地形尺寸 XYZ方块房间文件
default20260924single3 / stonegarden46 × 48 × 581678715schematic · 参数
cliff_courtyard0courtyard2 / nonecliff55 × 54 × 613179210schematic · 参数
winter_garden_crag2single2 / nonecliff42 × 50 × 56204359schematic · 参数
terrace_tower8ell1 / timberterrace43 × 37 × 61168386schematic · 参数
three_wing_inn13twin3 / timberterrace51 × 49 × 562055012schematic · 参数
cross_gabled_lodge21ell2 / timbergarden43 × 46 × 531464911schematic · 参数
woodland_house31single2 / nonegarden35 × 40 × 4488155schematic · 参数
watchtower_inn37staggered3 / stonegarden51 × 47 × 702123917schematic · 参数
twin_garden_wings20260923twin2 / noneterrace59 × 43 × 50186768schematic · 参数
compact_cottage907single1 / nonegarden33 × 32 × 4171083schematic · 参数
cross_roof_conservatory907ell1 / stoneterrace49 × 35 × 52152449schematic · 参数

原始样本 manifest · 下载生成器、测试与说明

10 · 测试通过意味着什么

harness 最终重跑:139 passed in 37.10s。测试由同一 agent 编写,harness 执行。 这是可复查的执行证据,不是独立设计的美学基准。

范围证据未覆盖的结论
104 个 seed0–99、正负 64 位边界、默认 seed、20260923不等于整个 seed 空间
8 个目标配置定向覆盖体量和扩建交互不等于任意参数笛卡尔积
实际几何门床配对、支撑、楼梯净高、可达地板、窗户、外壳 flood-fill不是 Minecraft 完整物理模拟
破坏性案例故意破坏门、床、楼梯、梯子支撑、窗户、入口和屋顶,要求检查失败不证明所有缺陷都能识别
确定性与读回同输入重复导出逐字节一致,读回再次检查语义原环境的结论;网页导出另做互操作验证
harness 的 pytest 原文
........................................................................ [ 51%] ................................................................... [100%] 139 passed in 37.10s

11 · 迭代中暴露的问题

75 条 shell 命令中 7 条非零退出。下面按实际日志展示,不把一次失败命令等同于一个独立缺陷,也不把捕获异常后退出 0 的扫描当成全通过。

为什么 changed on reload 不能直接归因于覆盖方块

MCIO 创建材质保留属性插入顺序,读回排序。属性和值相同也可能字符串不等。 standard toolset 已在本次运行前改用解析后的普通字典比较;MCIO NBT 对象自身的相等比较也不能直接替代这一步。 最终测试同时覆盖属性重排和同坐标覆盖写入。该检查属于工具模板提供的基础逻辑,不能全部记为模型原创。

#14 · 2026-09-24T21:57:18.684Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/plan.py' s=open(p).read(); s=s.replace("ex = w // 2 +", "free_sides = [side for side in ('left', 'right') if not any(v.name == 'wing_' + side for v in volumes) and not (side == 'right' and p['conservatory'])]\n if not free_sides:\n condition('porch', ('entry', 'full'), 'no free side for a wraparound gallery')\n p['wrap_side'] = free_sides[0] if free_sides and p['porch'] == 'wrap' else None\n ex = w // 2 +") open(p,'w').write(s) p='blocks.py';s=open(p).read().replace('"name": "build"','"name": "Procedural woodland lodge"').replace('"description": "Generated build"','"description": "Seeded furnished lodge, Java 1.21.1"');open(p,'w').write(s) PY cat > lodge/validation.py <<'PY' # Validate actual final block geometry, physical interfaces, and traversable interiors. from __future__ import annotations from collections import deque, Counter from functools import lru_cache from .geometry import STATES, DIRECTIONS, name_of @lru_cache(None) def parse(state): name = name_of(state) props = {} if state and '[' in state: props = dict(item.split('=') for item in state.split('[', 1)[1][:-1].split(',')) return name, props def walk_passable(name): return name in ('air', 'ladder', 'vine') or name.endswith('_door') def solid_support(name): return name != 'air' and name not in ('ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass') and not name.endswith(('_fence', '_wall', '_door', '_trapdoor', '_slab', '_leaves', '_bed')) and not name.startswith('potted_') def reachable(scene): """Flood walkable standing cells using stair orientation and supported ladders. Args: scene (Scene): Final geometry after every decorative pass. Returns: set: Reachable feet coordinates. Doors are player-operable, closed by default. """ blocks = scene.blocks def at(x, y, z): return name_of(blocks.get((x, y, z))) candidates = set() for (x, y, z), state in blocks.items(): n = name_of(state) if solid_support(n) or n.endswith('_stairs'): candidates.add((x, y + 1, z)) if n == 'ladder': candidates.add((x, y, z)) candidates.add((x, y + 1, z)) nodes = {p for p in candidates if walk_passable(at(*p)) and walk_passable(at(p[0], p[1] + 1, p[2]))} start = scene.plan.approach[0] first = (start['x'], start['y'] + 1, start['z']) assert first in nodes, f'exterior start obstructed {first}: {at(*first)}' seen = {first} queue = deque([first]) while queue: x, y, z = queue.popleft() for face, (dx, dz) in DIRECTIONS.items(): for dy in (0, 1, -1): q = (x + dx, y + dy, z + dz) if q not in nodes or q in seen: continue if dy: stairp = (q[0], q[1] - 1, q[2]) if dy > 0 else (x, y - 1, z) n, props = parse(blocks.get(stairp)) required = face if dy > 0 else next(k for k, d in DIRECTIONS.items() if d == (-dx, -dz)) if not n.endswith('_stairs') or props.get('facing') != required or props.get('half') != 'bottom': continue seen.add(q) queue.append(q) for dy in (-1, 1): q = (x, y + dy, z) if q in nodes and q not in seen and (at(x, y, z) == 'ladder' or at(*q) == 'ladder'): seen.add(q) queue.append(q) return seen def envelope_leaks(scene): """Flood exterior air through porous blocks and detect exposed room targets.""" # Occupied room components share a closed envelope; report the first leaking target. ox, _, oz = scene.plan.origin sx, sy, sz = scene.plan.size_xyz xmin, xmax, zmin, zmax = -ox, sx - ox - 1, -oz, sz - oz - 1 porous = {'air', 'ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass', 'flower_pot', 'lightning_rod'} barriers = set() for p, state in scene.blocks.items(): n = name_of(state) if n in porous or n.endswith(('_stairs', '_slab', '_fence', '_wall', '_leaves', '_bed')) or n.startswith('potted_'): continue barriers.add(p) # Flood from every boundary face, with a compact flat integer visited set. plane = sx * sz def idx(x, y, z): return y * plane + (z + oz) * sx + x + ox blocked = bytearray(sx * sy * sz) for x, y, z in barriers: if xmin <= x <= xmax and zmin <= z <= zmax and 0 <= y < sy: blocked[idx(x, y, z)] = 1 seen = bytearray(len(blocked)) queue = deque() def add(i): if not blocked[i] and not seen[i]: seen[i] = 1 queue.append(i) for y in range(sy): for z in range(sz): for x in (0, sx - 1): add(y * plane + z * sx + x) for x in range(sx): for z in (0, sz - 1): add(y * plane + z * sx + x) for z in range(sz): for x in range(sx): add(z * sx + x) add((sy - 1) * plane + z * sx + x) while queue: i = queue.popleft() y, rem = divmod(i, plane) z, x = divmod(rem, sx) if x: add(i - 1) if x < sx - 1: add(i + 1) if z: add(i - sx) if z < sz - 1: add(i + sx) if y: add(i - plane) if y < sy - 1: add(i + plane) return [p for p in scene.enclosed if seen[idx(*p)]] def validate_scene(scene, envelope=True): """Check final geometry, without trusting the plan's declared room graph.""" blocks = scene.blocks get = scene.get p = scene.plan ox, oy, oz = p.origin sx, sy, sz = p.size_xyz assert all(0 <= x + ox < sx and 0 <= y + oy < sy and 0 <= z + oz < sz for x, y, z in blocks), 'bounds violation' for state in set(blocks.values()): n, props = parse(state) assert n in STATES, state domains, defaults = STATES[n] assert set(props) == set(defaults), state assert all(value in domains[key] for key, value in props.items()), state doors, beds = 0, 0 for (x, y, z), state in blocks.items(): n, props = parse(state) if n.endswith('_door'): other = (x, y + (1 if props['half'] == 'lower' else -1), z) nn, pp = parse(blocks.get(other)) assert n == nn and pp.get('half') != props['half'] and all(props[k] == pp[k] for k in ('facing', 'hinge', 'open', 'powered')), f'broken door {(x,y,z)}' if props['half'] == 'lower': assert solid_support(get(x, y - 1, z)), f'unsupported door {(x,y,z)}' doors += 1 if n.endswith('_bed'): dx, dz = DIRECTIONS[props['facing']] sign = 1 if props['part'] == 'foot' else -1 nn, pp = parse(blocks.get((x + sign * dx, y, z + sign * dz))) assert nn == n and pp.get('part') != props['part'] and pp.get('facing') == props['facing'], f'broken bed {(x,y,z)}' assert solid_support(get(x, y - 1, z)), f'unsupported bed {(x,y,z)}' beds += props['part'] == 'foot' if n == 'ladder': dx, dz = DIRECTIONS[props['facing']] assert solid_support(get(x - dx, y, z - dz)), f'unsupported ladder {(x,y,z)}' if n == 'lantern': dy = 1 if props['hanging'] == 'true' else -1 assert get(x, y + dy, z) not in ('air', 'vine', 'glass'), f'unsupported lantern {(x,y,z)}' if n.startswith('potted_') or n in ('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet'): assert solid_support(get(x, y - 1, z)), f'unsupported plant {(x,y,z)}' if n == 'vine': assert any(props.get(face) == 'true' and solid_support(get(x + dx, y, z + dz)) for face, (dx, dz) in DIRECTIONS.items()), f'unattached vine {(x,y,z)}' for q, support in scene.attachments: assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' for window in scene.windows: assert all('glass' in get(*q) for q in window['cells']), f'overwritten window {window}' assert all(get(*q) == 'air' for q in window['inside'] + window['outside']), f'blocked window {window}' for x, y, z, facing in scene.stairs: n, props = parse(blocks.get((x, y, z))) assert n.endswith('_stairs') and props['facing'] == facing, f'overwritten tread {(x,y,z)}' assert solid_support(get(x, y - 1, z)), f'unsupported tread {(x,y,z)}' assert all(walk_passable(get(x, y + dy, z)) for dy in (1, 2, 3)), f'stair headroom {(x,y,z)}' visited = reachable(scene) for room in scene.rooms: assert room['target'] in visited, f'unreachable {room["volume"]}/{room["purpose"]}: {room["target"]}' assert room['furniture'], f'unfurnished room {room}' assert room['lights'], f'unlit room {room}' assert all(get(*q) == 'lantern' for q in room['lights']), f'overwritten room lights {room}' assert all(abs(q[1] - room['floor']) <= 3 for q in room['lights']), f'light too high {room}' assert beds >= 1, 'no sleeping accommodation' if envelope: leaks = envelope_leaks(scene) assert not leaks, f'weather envelope leaks to {leaks}' return {'rooms': len(scene.rooms), 'beds': beds, 'doors': doors, 'windows': len(scene.windows), 'walking_stairs': len(scene.stairs), 'reachable_cells': len(visited), 'blocks': sum(name_of(s) != 'air' for s in blocks.values()), 'envelope_checked': envelope} PY /opt/venv/bin/python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 174, in validate_scene assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: detached decoration (6, 11, 19) -> (6, 11, 20)
#16 · 2026-09-24T21:57:50.114Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace("if any(c in self.reserved for c in cells + inside):", "if any(c in self.reserved for c in cells + inside + outside):").replace("if any(self.get(*q) not in ('air',) for q in inside + outside):", "if any(any(v.interior(*q) for v in self.plan.volumes) for q in outside):\n return\n if any(self.get(*q) not in ('air',) for q in inside + outside):");open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 174, in validate_scene assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: detached decoration (15, 29, 23) -> (14, 29, 23)
#17 · 2026-09-24T21:58:07.742Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read();s=s.replace("for link in p.links:\n", "for v in p.volumes:\n if v.kind == 'stone':\n f = v.levels[-1]\n zz = (v.z0 + v.z1) // 2\n for x in range(v.x1 - 1, v.x1 + 3):\n for z in range(zz - 2, zz + 3):\n self.reserve((x, f + 1, z), 3)\n for link in p.links:\n",1);open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 193, in validate_scene assert not leaks, f'weather envelope leaks to {leaks}' ^^^^^^^^^ AssertionError: weather envelope leaks to [(9, 5, 5), (9, 5, 15), (9, 11, 5), (9, 11, 15), (9, 17, 5), (9, 17, 15), (9, 23, 10), (9, 5, 25), (9, 11, 25), (9, 17, 25), (9, 23, 25), (9, 29, 25), (22, 5, 10)]
#20 · 2026-09-24T21:58:44.180Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace("self.doors.append((x, y, z))", "if self.get(x, y + 2, z) == 'air':\n self.p(x, y + 2, z, self.glass)\n self.doors.append((x, y, z))");open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 193, in validate_scene assert not leaks, f'weather envelope leaks to {leaks}' ^^^^^^^^^ AssertionError: weather envelope leaks to [(9, 5, 5), (9, 5, 15), (9, 11, 5), (9, 11, 15), (9, 17, 5), (9, 17, 15), (9, 23, 10), (9, 5, 25), (9, 11, 25), (9, 17, 25), (9, 23, 25), (9, 29, 25), (22, 5, 10)]
#22 · 2026-09-24T21:59:28.788Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read();s=s.replace(' def route_reservations(self):', " def roof_covered(self, volume, x, y, z):\n return any(v is not volume and v.contains(x, z) and v.base <= y < v.roof(x, z) for v in self.plan.volumes)\n\n def route_reservations(self):") a=s.index(' overhang = 1');b=s.index(' def opening(',a);s=s[:a]+s[a:b].replace('self.occupied_elsewhere(', 'self.roof_covered(')+s[b:];open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 193, in validate_scene assert not leaks, f'weather envelope leaks to {leaks}' ^^^^^^^^^ AssertionError: weather envelope leaks to [(9, 5, 5), (9, 5, 15), (9, 11, 5), (9, 11, 15), (9, 17, 5), (9, 17, 15), (9, 23, 10), (9, 5, 25), (9, 11, 25), (9, 17, 25), (9, 23, 25), (9, 29, 25), (22, 5, 10)]
#52 · 2026-09-25T01:20:21.941Z · exit=1 · 14s · python - <<'PY'
python - <<'PY' p='lodge/validation.py' s=open(p).read().replace('from collections import deque, Counter','from collections import deque').replace('from functools import lru_cache','from functools import lru_cache\nimport hashlib\nimport json') a=s.index('\ndef validate_scene(') s=s[:a]+''' def isolated_floor_cells(scene, visited): """Find usable interior floor cells that final furniture or framing isolated.""" missed = set() for v in scene.plan.volumes: for f in v.levels: for x in range(v.x0 + 1, v.x1): for z in range(v.z0 + 1, v.z1): if solid_support(scene.get(x, f, z)) and all(walk_passable(scene.get(x, f + dy, z)) for dy in (1, 2)) and (x, f + 1, z) not in visited: missed.add((x, f + 1, z)) return missed def geometry_signature(scene): """Hash actual roof occupancy and room partitions, ignoring material noise. Args: scene (Scene): Final placed or reloaded geometry. Returns: str: Observed sample signature, not a count of possible designs. """ main = scene.plan.volumes[0] mask = set() for (x, y, z), state in scene.blocks.items(): if 'deepslate' in name_of(state): mask.add((x - main.x0, y - main.base, z - main.z0, 'roof')) for volume in scene.plan.volumes: for floor in volume.levels: for x in range(volume.x0, volume.x1 + 1): for z in range(volume.z0, volume.z1 + 1): # Floor, wall and void geometry only. Decoration colour is irrelevant. if solid_support(scene.get(x, floor, z)): mask.add((x - main.x0, floor - main.base, z - main.z0, 'floor')) if not walk_passable(scene.get(x, floor + 1, z)): mask.add((x - main.x0, floor + 1 - main.base, z - main.z0, 'occupied')) return hashlib.sha256(json.dumps(sorted(mask), separators=(',', ':')).encode()).hexdigest() ''' + s[a:] s=s.replace(" assert beds >= 1, 'no sleeping accommodation'", " missed = isolated_floor_cells(scene, visited)\n assert not missed, f'isolated usable floor cells: {sorted(missed)[:12]}'\n for x, y, z in scene.doors:\n n, props = parse(blocks.get((x, y, z)))\n assert n.endswith('_door'), f'overwritten door {(x, y, z)}'\n dx, dz = DIRECTIONS[props['facing']]\n for sign in (-1, 1):\n assert all(walk_passable(get(x + sign * dx, y + dy, z + sign * dz)) for dy in (0, 1)), f'blocked doorway {(x, y, z)}'\n for v in p.volumes:\n if v.kind != 'dormer':\n assert all(solid_support(get(x, y, z)) for x in range(v.x0, v.x1 + 1) for z in range(v.z0, v.z1 + 1) for y in range(v.base)), f'discontinuous foundation {v.name}'\n assert beds >= 1, 'no sleeping accommodation'") open(p,'w').write(s) p='lodge/plan.py';s=open(p).read().replace(' limits: dict\n', ' limits: dict\n overrides: dict = field(default_factory=dict)\n').replace('trees, origin, size, lim)', 'trees, origin, size, lim, overrides)');open(p,'w').write(s) p='generate.py';s=open(p).read().replace(" record['structural_signature'] = self.plan.signature()", " record['structural_signature'] = self.plan.signature()\n from lodge.validation import geometry_signature\n record['placed_geometry_signature'] = geometry_signature(self.scene)");open(p,'w').write(s) PY cat > test/conftest.py <<'PY' # Keep imports and all ephemeral test output within the delivered workspace. import sys import tempfile from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) @pytest.fixture def artifact_dir(): """Create and clean a scratch directory beneath test/.""" with tempfile.TemporaryDirectory(prefix='artifacts-', dir=ROOT / 'test') as directory: yield Path(directory) PY cat > test/cases.py <<'PY' # Deliberate boundaries and feature intersections supplement the ordinary seed corpus. TARGETS = [ ('small_cottage', {'topology': 'single', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'z', 'profile': 'chalet', 'wing_floors': 1, 'tower': 'none', 'conservatory': False, 'porch': 'full', 'balcony': False, 'dormers': 0, 'terrain': 'garden'}), ('narrow_cross_dormers', {'topology': 'ell', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'x', 'profile': 'chalet', 'wing_size': 9, 'wing_floors': 1, 'tower': 'stone', 'tower_width': 9, 'conservatory': True, 'porch': 'entry', 'balcony': False, 'dormers': 2, 'terrain': 'terrace'}), ('cliff_courtyard', {'topology': 'courtyard', 'width': 19, 'depth': 21, 'floors': 3, 'ridge': 'x', 'profile': 'steep', 'wing_size': 13, 'wing_floors': 2, 'tower': 'stone', 'tower_width': 11, 'tower_extra': 2, 'conservatory': True, 'porch': 'full', 'balcony': True, 'dormers': 2, 'terrain': 'cliff', 'layout': 'suites'}), ('long_inn', {'topology': 'staggered', 'width': 19, 'depth': 21, 'floors': 3, 'ridge': 'z', 'profile': 'bell', 'wing_size': 13, 'wing_floors': 2, 'tower': 'timber', 'tower_width': 11, 'tower_extra': 2, 'conservatory': True, 'porch': 'full', 'balcony': True, 'dormers': 2, 'terrain': 'cliff'}), ('glass_wrap', {'topology': 'ell', 'width': 15, 'depth': 17, 'floors': 2, 'ridge': 'x', 'profile': 'chalet', 'wing_floors': 1, 'tower': 'none', 'conservatory': True, 'porch': 'wrap', 'balcony': True, 'dormers': 2, 'terrain': 'garden', 'entrance': 'left'}), ('short_watchtower', {'topology': 'single', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'z', 'profile': 'chalet', 'wing_floors': 1, 'tower': 'stone', 'tower_width': 11, 'tower_extra': 2, 'conservatory': False, 'porch': 'entry', 'balcony': False, 'dormers': 0, 'terrain': 'garden'}), ('cross_twin', {'topology': 'twin', 'width': 17, 'depth': 19, 'floors': 2, 'ridge': 'x', 'profile': 'bell', 'wing_size': 9, 'wing_floors': 2, 'wing_ridge': 'cross', 'placement': 'front', 'tower': 'none', 'conservatory': False, 'porch': 'entry', 'balcony': False, 'dormers': 1, 'layout': 'open'}), ('rear_gallery', {'topology': 'ell', 'width': 19, 'depth': 21, 'floors': 3, 'ridge': 'z', 'profile': 'steep', 'wing_size': 9, 'wing_floors': 2, 'placement': 'rear', 'tower': 'stone', 'tower_extra': 1, 'conservatory': False, 'porch': 'wrap', 'balcony': True, 'dormers': 2, 'layout': 'suites'}), ] PY cat > test/test_generator.py <<'PY' # Exercise real block geometry over a finite corpus, targeted cases, and deliberate defects. from __future__ import annotations import copy import json import subprocess import sys from pathlib import Path import pytest from generate import Build, generate, blockstates_equivalent from blocks import load_schematic from lodge.plan import DEFAULT_SEED, DOMAINS, resolve from lodge.geometry import DIRECTIONS, blockstate, name_of from lodge.validation import validate_scene, envelope_leaks, geometry_signature, parse, reachable from cases import TARGETS CORPUS = list(range(100)) + [-(2**63), 2**63 - 1, DEFAULT_SEED, 20260923] @pytest.mark.parametrize('seed', CORPUS) def test_seed_corpus(seed): build = generate(seed) scene = build.scene for room in scene.rooms: x0, y0, z0, x1, y1, z1 = room['bounds'] contents = {name_of(state) for (x, y, z), state in scene.blocks.items() if x0 <= x <= x1 and y0 <= y <= y1 and z0 <= z <= z1} if 'kitchen' in room['purpose']: assert 'smoker' in contents and 'crafting_table' in contents, (seed, room) if 'bed' in room['purpose'] or 'suite' in room['purpose']: assert any(name.endswith('_bed') for name in contents), (seed, room) visited = reachable(scene) for foot in scene.beds: x, y, z = foot _, props = parse(scene.blocks[foot]) dx, dz = DIRECTIONS[props['facing']] assert any((xx + a, y, zz + b) in visited for xx, zz in ((x, z), (x + dx, z + dz)) for a, b in DIRECTIONS.values()), (seed, foot) @pytest.mark.parametrize('name,parameters', TARGETS, ids=[n for n, _ in TARGETS]) def test_targeted_geometry(name, parameters): build = generate(907, parameters) assert build.validation['envelope_checked'] assert all(any(r['volume'] == v.name and r['floor'] == f for r in build.scene.rooms) for v in build.plan.volumes for f in v.levels) @pytest.mark.parametrize('seed', [0, 8, 31, DEFAULT_SEED, -(2**63), 2**63 - 1]) def test_determinism_and_semantic_roundtrip(seed, artifact_dir): a = generate(seed, output=artifact_dir / 'a.litematic') b = generate(seed, output=artifact_dir / 'b.litematic') assert a.placed == b.placed assert (artifact_dir / 'a.litematic').read_bytes() == (artifact_dir / 'b.litematic').read_bytes() assert a.plan.record() == b.plan.record() loaded = load_schematic(artifact_dir / 'a.litematic') assert loaded.size_yzx == (a.size_y, a.size_z, a.size_x) ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) ox, oy, oz = a.plan.origin scene = copy.copy(a.scene) scene.blocks = {p: loaded.palette[int(ids[p[1] + oy, p[2] + oz, p[0] + ox])] for p in a.scene.blocks} assert geometry_signature(scene) == geometry_signature(a.scene) validate_scene(scene) record = json.loads((artifact_dir / 'a.json').read_text()) replay = generate(record['seed'], record['overrides']) assert replay.placed == a.placed def test_semantic_property_comparison_and_overwrite(artifact_dir): left = 'minecraft:oak_stairs[facing=north,half=bottom,shape=straight]' reordered = 'minecraft:oak_stairs[shape=straight,half=bottom,facing=north]' assert blockstates_equivalent(left, reordered) assert not blockstates_equivalent(left, left.replace('north', 'south')) assert not blockstates_equivalent(left, 'minecraft:oak_stairs[facing=north,half=bottom]') assert not blockstates_equivalent('minecraft:stone', 'minecraft:cobblestone') build = Build(31).build() build.put(0, 0, 0, 'minecraft:dirt') build.put(0, 0, 0, 'minecraft:oak_stairs', facing='north', half='bottom') build.export(artifact_dir / 'overwrite.litematic') build.verify(artifact_dir / 'overwrite.litematic') with pytest.raises(AssertionError, match='out of bounds'): build.put(-1, 0, 0, 'minecraft:stone') @pytest.mark.parametrize('params', [ {'width': 16}, {'floors': True}, {'unknown': 1}, {'floors': 1, 'wing_floors': 2}, {'floors': 1, 'balcony': True}, {'topology': 'courtyard', 'porch': 'wrap'}, ]) def test_invalid_explicit_parameters(params): with pytest.raises(ValueError): resolve(0, params) @pytest.mark.parametrize('seed', [True, -(2**63) - 1, 2**63, '7']) def test_seed_domain(seed): with pytest.raises(ValueError, match='signed 64-bit'): resolve(seed) def test_resource_limits_are_enforced(): with pytest.raises(ValueError, match='resource limits'): resolve(0, limits={'max_axis': 20}) with pytest.raises(ValueError, match='floor_height'): resolve(0, limits={'floor_height': 5}) with pytest.raises(ValueError, match='unknown resource limit'): resolve(0, limits={'hidden': 100}) with pytest.raises(ValueError, match='not a Java'): blockstate('unobtainium') with pytest.raises(ValueError, match='invalid'): blockstate('oak_stairs', (('facing', 'up'),)) def test_observed_structural_diversity(): signatures = set() coverage = {k: set() for k in ('topology', 'floors', 'ridge', 'profile', 'tower', 'conservatory', 'porch', 'balcony', 'dormers', 'terrain', 'entrance', 'layout', 'stair_side')} for seed in range(40): b = Build(seed).build() signatures.add(geometry_signature(b.scene)) for key in coverage: coverage[key].add(b.plan.parameters[key]) assert len(signatures) == 40 for key, observed in coverage.items(): assert observed == set(DOMAINS[key]), (key, observed) # Change a genuine room-division parameter under the same massing. params = dict(TARGETS[0][1], floors=2) a = Build(31, dict(params, layout='open')).build() b = Build(31, dict(params, layout='suites')).build() assert a.plan.signature() == b.plan.signature() assert geometry_signature(a.scene) != geometry_signature(b.scene) @pytest.mark.parametrize('defect', ['door', 'bed', 'stair_headroom', 'ladder_support', 'window', 'entry']) def test_actual_geometry_defects_are_rejected(defect): scene = Build(DEFAULT_SEED).build().scene if defect == 'door': x, y, z = scene.doors[0] scene.p(x, y + 1, z, 'air') elif defect == 'bed': x, y, z = scene.beds[0] scene.p(x, y, z, 'air') elif defect == 'stair_headroom': x, y, z, _ = scene.stairs[-1] scene.p(x, y + 2, z, 'stone') elif defect == 'ladder_support': x, y, z = scene.ladders[0] _, props = parse(scene.blocks[x, y, z]) dx, dz = DIRECTIONS[props['facing']] scene.p(x - dx, y, z - dz, 'air') elif defect == 'window': scene.p(*scene.windows[0]['cells'][0], 'stone') else: x, y, z = scene.plan.entrance scene.box((x, y, z, x, y + 2, z), 'stone') with pytest.raises(AssertionError): validate_scene(scene) def test_envelope_defect_is_rejected(): scene = Build(907, TARGETS[0][1]).build().scene v = scene.plan.volumes[0] x, z = (v.x0 + v.x1) // 2, (v.z0 + v.z1) // 2 h = v.roof(x, z) scene.clear((x, h - 2, z, x, h + 3, z)) assert envelope_leaks(scene) def test_cli_clean_directory(artifact_dir): script = Path(__file__).resolve().parents[1] / 'generate.py' subprocess.run([sys.executable, str(script), '--seed', '31'], cwd=artifact_dir, check=True, capture_output=True) assert (artifact_dir / 'output.litematic').is_file() assert json.loads((artifact_dir / 'output.json').read_text())['seed'] == 31 PY /opt/venv/bin/python -m pytest -q test --disable-warnings --maxfail=4

记录中的输出末尾

rpose']: > assert 'smoker' in contents and 'crafting_table' in contents, (seed, room) E AssertionError: (26, {'volume': 'main', 'floor': 4, 'purpose': 'hearth kitchen and dining', 'bounds': (1, 5, 1, 17, 8, 15), ...}) E assert ('smoker' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...} and 'crafting_table' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...}) test/test_generator.py:29: AssertionError _____________________________ test_seed_corpus[60] _____________________________ seed = 60 @pytest.mark.parametrize('seed', CORPUS) def test_seed_corpus(seed): build = generate(seed) scene = build.scene for room in scene.rooms: x0, y0, z0, x1, y1, z1 = room['bounds'] contents = {name_of(state) for (x, y, z), state in scene.blocks.items() if x0 <= x <= x1 and y0 <= y <= y1 and z0 <= z <= z1} if 'kitchen' in room['purpose']: > assert 'smoker' in contents and 'crafting_table' in contents, (seed, room) E AssertionError: (60, {'volume': 'main', 'floor': 4, 'purpose': 'hearth kitchen and dining', 'bounds': (1, 5, 1, 15, 8, 19), ...}) E assert ('smoker' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...} and 'crafting_table' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...}) test/test_generator.py:29: AssertionError =========================== short test summary info ============================ FAILED test/test_generator.py::test_seed_corpus[0] - AssertionError: (0, {'vo... FAILED test/test_generator.py::test_seed_corpus[16] - AssertionError: isolate... FAILED test/test_generator.py::test_seed_corpus[26] - AssertionError: (26, {'... FAILED test/test_generator.py::test_seed_corpus[60] - AssertionError: (60, {'... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 4 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 4 failed, 57 passed in 14.05s
#58 · 2026-09-25T01:25:01.352Z · exit=128 · 0s · rg --files previews | wc -l && rg --files previews | head -18 && git status --short
rg --files previews | wc -l && rg --files previews | head -18 && git status --short

记录中的输出末尾

57 previews/index.html previews/cross_roof_conservatory/stair_section.camera.json previews/cross_roof_conservatory/stair_section.png previews/cross_roof_conservatory/ground_cutaway.camera.json previews/cross_roof_conservatory/ground_cutaway.png previews/cross_roof_conservatory/rear.png previews/cross_roof_conservatory/front.png previews/compact_cottage/stair_section.camera.json previews/compact_cottage/stair_section.png previews/compact_cottage/ground_cutaway.camera.json previews/compact_cottage/ground_cutaway.png previews/compact_cottage/rear.png previews/compact_cottage/front.png previews/cross_gabled_lodge/stair_section.camera.json previews/cross_gabled_lodge/stair_section.png previews/cross_gabled_lodge/ground_cutaway.camera.json previews/cross_gabled_lodge/ground_cutaway.png previews/cross_gabled_lodge/rear.png fatal: not a git repository (or any parent up to mount point /) Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).

12 · 完整命令轨迹

时间均为 UTC。展开查看完整命令及日志中保留的输出末尾。序号贯穿关机前后。

#01 · 2026-09-24T21:43:14.764Z · exit=0 · 0s · 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/ref-5/generate.py ./references/ref-1/generate.py ./references/README.md ./references/ref-8/generate.py ./references/ref-4/generate.py ./references/ref-7/generate.py /work/generator/workspace/references/ref-5/generate.py ./references/ref-3/generate.py /work/generator/workspace/references/ref-1/generate.py /work/generator/workspace/references/README.md ./references/ref-6/generate.py /work/generator/workspace/references/ref-8/generate.py ./references/ref-2/generate.py ./generate.py ./AGENTS.md /work/generator/workspace/generate.py /work/generator/workspace/AGENTS.md /work/generator/workspace/references/ref-4/generate.py /work/generator/workspace/references/ref-7/generate.py /work/generator/workspace/references/ref-2/generate.py /work/generator/workspace/references/ref-3/generate.py /work/generator/workspace/references/ref-6/generate.py
#02 · 2026-09-24T21:43:14.843Z · exit=0 · 0s · cat /work/AGENTS.md && cat generate.py && cat references/README.md
cat /work/AGENTS.md && cat generate.py && cat references/README.md

记录中的输出末尾

ce_ref-2.png` | `prompts/ref-2/task/reference.png` | `a0c12cba6c366f7b623460853b0df3742f4f07d7395ed4a3dd2fcd3743caa122` | | `references/ref-3/generate.py` | `runs/standard/ref-3/20260923T212721/work/generator/workspace/generate.py` | `750f0b10a3f69ecdb29608259a159c7caec3f0443f7a10df1d561cde1b0b8ab8` | | `reference_ref-3.png` | `prompts/ref-3/task/reference.png` | `1eb77af8d74c4f29d5c0c30abdf16bf1917b68e776e6a0dfe8e4e50541a81fb4` | | `references/ref-4/generate.py` | `runs/standard/ref-4/20260923T212819/work/generator/workspace/generate.py` | `d1d8c312871a59f2f11afa94f5370f025b745e248f55726b45294783fc5c6533` | | `reference_ref-4.png` | `prompts/ref-4/task/reference.png` | `f29b25707d7563197e3d5440e78f3d13cef38101be752b5174187812a3e067b0` | | `references/ref-5/generate.py` | `runs/standard/ref-5/20260923T214601/work/generator/workspace/generate.py` | `161193d9b58feeac4d59b646d39d0606d5be53210bf88c9d6edfb2d34822048e` | | `reference_ref-5.png` | `prompts/ref-5/task/reference.png` | `6a978df56439a5136b565687199a112d80cfc982a00df4b4f06f54250278d998` | | `references/ref-6/generate.py` | `runs/standard/ref-6/20260923T214606/work/generator/workspace/generate.py` | `bace3e591f8dfdb61d09968d13e9176a30432da4715c2eae3f382651ebec1010` | | `reference_ref-6.png` | `prompts/ref-6/task/reference.png` | `3a22f1581bc8b72fddd5594345b6573156ef7ccdf74936c31cdb86613ec4442d` | | `references/ref-7/generate.py` | `runs/standard/ref-7/20260923T214612/work/generator/workspace/generate.py` | `71838ba26c2801f92fd01fab70847c20a7fef585fa56f3cbc921d34364e3bad4` | | `reference_ref-7.png` | `prompts/ref-7/task/reference.png` | `4a6d6ab0ff861a8ffcd38d728a4e47c2af31bcdc15fa1676f7d5b4e12384aad5` | | `references/ref-8/generate.py` | `runs/standard/ref-8/20260923T214616/work/generator/workspace/generate.py` | `2bd08d3038d4eedde2d9811a1e264fb8eff35d0f6f21b52b8e14772f3d59dcd3` | | `reference_ref-8.png` | `prompts/ref-8/task/reference.png` | `ea93850446545fcdccc6400a4060fd462caaa6d15a448d77525c780e87edf52f` |
#03 · 2026-09-24T21:43:21.205Z · exit=0 · 0s · wc -l references/ref-{1,2,3,4,5,6,7,8}/generate.py && rg -n '^ def |^def |CONFIG|size_xyz' references/ref-{
wc -l references/ref-{1,2,3,4,5,6,7,8}/generate.py && rg -n '^ def |^def |CONFIG|size_xyz' references/ref-{1,2,3,4,5,6,7,8}/generate.py

记录中的输出末尾

def door(self, x, y, z, facing='south', hinge='left'): references/ref-6/generate.py:195: def bed(self, x, y, z, facing='north', color='red'): references/ref-6/generate.py:201: def window(self, axis, fixed, lo, hi, y0, y1, exterior=1): references/ref-6/generate.py:229: def room_box(self, x0, z0, x1, z1, floor, ceiling): references/ref-6/generate.py:246: def occupied_tower(self, x, y, z): references/ref-6/generate.py:250: def gable_roof(self, x0, z0, x1, z1, eave, axis='z', skip=None): references/ref-6/generate.py:279: def hip_apron(self, x0, z0, x1, z1, y0, steps): references/ref-6/generate.py:292: def table(self, x, y, z, long=1): references/ref-6/generate.py:299: def planter(self, x, y, z, flower='fern'): references/ref-6/generate.py:307: def terrain(self): references/ref-6/generate.py:352: def architecture(self): references/ref-6/generate.py:526: def furnishings(self): references/ref-6/generate.py:634: def exterior(self): references/ref-6/generate.py:734: def conifer(self, x, z, base, height): references/ref-6/generate.py:758: def finish_details(self): references/ref-6/generate.py:823: def build(self) -> 'Build': references/ref-6/generate.py:835: def export(self, path: Path) -> Path: references/ref-6/generate.py:845: name=CONFIG["name"], references/ref-6/generate.py:846: author=CONFIG["author"], references/ref-6/generate.py:847: description=CONFIG["description"], references/ref-6/generate.py:848: minecraft_data_version=CONFIG["data_version"], references/ref-6/generate.py:857: def verify(self, path: Path) -> Dict[str, object]: references/ref-6/generate.py:876: "size_xyz": [size_x, size_y, size_z], references/ref-6/generate.py:882:def main() -> None: references/ref-6/generate.py:885: parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) references/ref-6/generate.py:886: parser.add_argument("--seed", type=int, default=CONFIG["seed"])
#04 · 2026-09-24T21:43:21.289Z · exit=0 · 0s · cat /opt/tools/template_render.mjs && head -c 2500 /work/generator/MCRender/resources/1.21.1-blocks.json
cat /opt/tools/template_render.mjs && head -c 2500 /work/generator/MCRender/resources/1.21.1-blocks.json

记录中的输出末尾

alse"],"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","half":"bottom","shape":"straight","waterlogged":"false"}],"acacia_trapdoor":[{"facing":["north","south","west","east"],"half":["top","bottom"],"open":["true","false"],"powered":["true","false"],"waterlogged":["true","false"]},{"facing":"north","half":"bottom","open":"false","powered":"false","waterlogged":"false"}],"acacia_wall_hanging_sign":[{"facing":["north","south","west","east"],"waterlogged":["true","false"]},{"facing":"north","waterlogged":"false"}],"acacia_wall_sign":[{"facing":["north","south","west","east"],"
#05 · 2026-09-24T21:43:27.731Z · exit=0 · 0s · sed -n '168,746p' references/ref-1/generate.py && sed -n '158,704p' references/ref-2/generate.py
sed -n '168,746p' references/ref-1/generate.py && sed -n '158,704p' references/ref-2/generate.py

记录中的输出末尾

nt='true', distance='1') elif chance < .56: self.block(x, 2, z, 'mossy_cobblestone') # Clusters on the upper garden terrace. for x0, z0 in [(9, 11), (9, 32), (12, 43), (18, 43), (36, 43), (42, 43), (46, 11)]: for dx in (-1, 0, 1): x = x0 + dx if self.get(x, 4, z0) in (None, 'minecraft:air'): self.block(x, 4, z0, 'flowering_azalea_leaves' if dx == 0 else 'azalea_leaves', persistent='true', distance='1') # Small log stack at the west side, all resting on paving. self.mix((8, 3, 20, 9, 4, 23), STONE) for z in range(20, 24): for y in (5, 6): self.block(9, y, z, 'spruce_log', axis='x') # Front climbing vine stems and leaf clusters attached to corner posts. for x in (12, 27, 41): for y in range(6, 14): if self.get(x, y, 35) not in (None, 'minecraft:air') and self.get(x, y, 36) in (None, 'minecraft:air'): self.block(x, y, 36, 'vine', north='true') for x, y, z in [(11, 10, 35), (12, 11, 36), (26, 10, 35), (27, 11, 36), (41, 11, 36), (42, 10, 35)]: if self.get(x, y, z) in (None, 'minecraft:air'): self.block(x, y, z, 'oak_leaves', persistent='true', distance='1') # Garden lamps on grounded posts, corresponding to the warm boundary lights. for x, z in [(7, 39), (47, 38), (9, 8), (46, 8)]: self.block(x, 4, z, 'stone_bricks') self.fill((x, 5, z, x, 7, z), 'spruce_fence') self.lamp(x, 8, z) def build(self) -> 'Build': """Build the reference cottage, grounds, and complete furnished interior.""" self.windows = [] self.lights = [] self.walking_stairs = [] self.terrain() self.frame() self.roofs() self.veranda() self.chimney() self.interiors() self.gardens() return self
#06 · 2026-09-24T21:43:35.694Z · exit=0 · 0s · sed -n '204,445p' references/ref-3/generate.py && sed -n '258,493p' references/ref-4/generate.py
sed -n '204,445p' references/ref-3/generate.py && sed -n '258,493p' references/ref-4/generate.py

记录中的输出末尾

self.log((10, 47, z, 21, 47, z), "x", "oak_log") self.log((15, 42, z, 15, 47, z), block="stripped_oak_log") for x in (10, 21): self.log((x, 47, 14, x, 47, 25), "z", "oak_log") self.box((9, 48, 13, 22, 48, 26), "minecraft:spruce_planks") self.lantern(15, 46, 20, True) self.put(15, 47, 20, "minecraft:oak_planks") self.box((18, 42, 19, 19, 42, 20), "minecraft:cartography_table") self.put(18, 43, 19, "minecraft:lantern", hanging="false") self.put(11, 42, 22, "minecraft:barrel", facing="up") # Square hipped roof with flared eaves, tiny summit and brass flagpole. for k in range(8): xmin, xmax, zmin, zmax = 7 + k, 24 - k, 11 + k, 28 - k y = 49 + k self.box((xmin, y, zmin, xmax, y, zmax), "minecraft:deepslate_tiles") for x in range(xmin, xmax + 1): self.stair(x, y, zmin, "cobbled_deepslate_stairs", "south") self.stair(x, y, zmax, "cobbled_deepslate_stairs", "north") for z in range(zmin + 1, zmax): self.stair(xmin, y, z, "deepslate_tile_stairs", "east") self.stair(xmax, y, z, "deepslate_tile_stairs", "west") self.box((15, 57, 19, 16, 57, 20), "minecraft:deepslate_tiles") self.put(15, 58, 19, "minecraft:deepslate_tile_wall", up="true") self.put(15, 59, 19, "minecraft:iron_bars") self.box((22, 50, 19, 22, 59, 19), "minecraft:oak_fence") self.put(22, 60, 19, "minecraft:lantern", hanging="false") for x in range(23, 28): for y in (57, 58, 59): if not (x == 27 and y == 58): self.put(x, y, 19, "minecraft:yellow_wool" if (x + y) % 3 else "minecraft:orange_wool") for name, y in (("tower pantry", 7), ("tower archive", 15), ("watchkeeper bunk", 24), ("map room", 33), ("open lookout", 42)): self.room_targets[name] = (14, y, 20) def interiors(self):
#07 · 2026-09-24T21:43:35.764Z · exit=0 · 0s · sed -n '178,277p' references/ref-5/generate.py && sed -n '345,527p' references/ref-5/generate.py && sed -n '24
sed -n '178,277p' references/ref-5/generate.py && sed -n '345,527p' references/ref-5/generate.py && sed -n '246,292p' references/ref-6/generate.py && sed -n '352,526p' references/ref-6/generate.py

记录中的输出末尾

a complete door. self.fill((9, 14, 20, 22, 19, 20), 'spruce_planks') self.log((9, 19, 20, 22, 19, 20), 'x') self.door(16, 14, 20, 'north') # Main staircase: seven supported treads, three clear blocks above each. for i in range(1, 8): y, z = 6 + i, 27 - i self.fill((25, 6, z, 26, y - 1, z), 'spruce_planks') self.air((25, y + 1, z, 26, y + 3, z)) for x in (25, 26): self.stair(x, y, z, 'north', walking=True) self.fill((25, 13, 18, 26, 13, 19), 'spruce_planks') self.air((25, 14, 18, 26, 16, 19)) # A full-height supported ladder serves the tower's five levels. self.log((38, 7, 10, 38, 37, 10)) for y in range(7, 36): self.block(38, y, 11, 'ladder', facing='south', waterlogged='false') self.block(38, y, 12, 'air') self.air((38, 36, 11, 38, 37, 12)) # The roof void is part of the upper room rather than an unserved floor. self.rooms = [ ('Taproom', (17, 7, 24), (9, 7, 21, 22, 12, 28)), ('Kitchen and larder', (16, 7, 16), (9, 7, 13, 22, 12, 19)), ('Entrance hall', (30, 7, 26), (27, 7, 21, 38, 12, 28)), ('Ground tower pantry', (36, 7, 14), (33, 7, 9, 41, 12, 18)), ('Garden parlour', (45, 7, 32), (41, 7, 26, 49, 12, 35)), ('South guest chamber', (16, 14, 24), (9, 14, 21, 22, 19, 28)), ('North guest chamber', (16, 14, 17), (9, 14, 13, 22, 19, 19)), ('Upper gallery', (30, 14, 24), (27, 14, 20, 39, 17, 27)), ('Tower map room', (36, 14, 14), (33, 14, 9, 41, 19, 18)), ('Garden guest room', (45, 14, 30), (41, 14, 26, 49, 17, 35)), ('Tower library', (36, 21, 14), (33, 21, 9, 41, 26, 18)), ('Tower study', (36, 28, 14), (34, 28, 10, 40, 33, 17)), ('Lookout chamber', (36, 35, 14), (35, 35, 11, 39, 38, 16)), ] def furnishings(self):
#08 · 2026-09-24T21:43:45.788Z · exit=0 · 0s · sed -n '269,330p' references/ref-7/generate.py && sed -n '428,562p' references/ref-7/generate.py && sed -n '29
sed -n '269,330p' references/ref-7/generate.py && sed -n '428,562p' references/ref-7/generate.py && sed -n '297,390p' references/ref-8/generate.py && sed -n '452,519p' references/ref-8/generate.py && sed -n '610,685p' references/ref-8/generate.py

记录中的输出末尾

, 13, 'stripped_spruce_log', axis='y') # Window boxes kept below the opening, with flowers on actual soil. for x0 in (13, 39): for floor in (6, 12, 18): for x in range(x0, x0 + 3): self.p(x, floor + 1, 15, 'grass_block') self.p(x, floor + 2, 15, 'poppy' if x % 2 else 'fern') self.p(x, floor + 1, 14, 'spruce_trapdoor', facing='north', half='bottom', open='true', powered='false', waterlogged='false') # Leaf garlands adhere to exposed timber and avoid glass and doors. for x, z, bottom, top in ((20, 13, 7, 21), (34, 13, 7, 23), (11, 15, 7, 19), (43, 15, 7, 22)): for y in range(bottom, top + 1): if self.get(x, y, z) in (None, 'minecraft:air'): self.p(x, y, z, 'oak_leaves', persistent='true', distance='1', waterlogged='false') if y % 3 == 0: xx = x + (-1 if x <= 27 else 1) if self.get(xx, y, z) in (None, 'minecraft:air'): self.p(xx, y, z, 'oak_leaves', persistent='true', distance='1', waterlogged='false') for z in (21, 28, 37): for y in range(7, 22): if self.get(44, y, z) in (None, 'minecraft:air'): self.p(44, y, z, 'vine', west='true', north='false', south='false', east='false', up='false') for x, z in ((9, 14), (45, 14), (9, 37), (45, 39)): self.p(x, 7, z, 'barrel', facing='up', open='false') self.p(x, 8, z, 'potted_azalea_bush') for x, z in ((18, 12), (36, 12), (9, 31), (45, 25)): if self.get(x, 7, z) in (None, 'minecraft:air'): self.p(x, 7, z, 'barrel', facing='up', open='false') self.stair(12, 7, 12, 'south') self.stair(13, 7, 12, 'south') self.stair(40, 7, 12, 'south') self.stair(41, 7, 12, 'south') def tree(self, x: int, z: int, height: int, radius: int) -> None:
#09 · 2026-09-24T21:43:45.861Z · exit=0 · 0s · sed -n '195,258p' references/ref-4/generate.py && sed -n '398,493p' references/ref-4/generate.py && sed -n '17
sed -n '195,258p' references/ref-4/generate.py && sed -n '398,493p' references/ref-4/generate.py && sed -n '178,277p' references/ref-5/generate.py

记录中的输出末尾

outes.append((xx, y + 1, zz)) else: self.put(xx, y, zz, 'minecraft:cobblestone') self.put(xx, y + 1, zz, 'minecraft:stone_brick_wall', up='true') self.ground[xx, zz] = y def approach(self) -> None: """Lay out the switchback ascent, kitchen stair and fenced lookout.""" self.landing(45, 50, 53, 55, 1) self.flight((46, 2, 52), 9, 'north') self.landing(43, 49, 40, 43, 11) self.flight((42, 12, 40), 9, 'west') self.landing(29, 33, 40, 43, 21) self.flight((30, 22, 39), 5, 'north') self.landing(21, 29, 33, 36, 26) self.landing(30, 33, 33, 34, 26) self.landing(10, 20, 34, 37, 26) self.flight((11, 16, 47), 10, 'north') self.landing(10, 25, 48, 51, 15) self.landing(11, 14, 37, 37, 26) # Each final riser occupies the landing edge, so no full-block jump is needed. for x in range(46, 50): self.put(x, 11, 43, 'minecraft:stone_brick_stairs', facing='north', half='bottom', shape='straight') for z in range(40, 44): self.put(33, 21, z, 'minecraft:stone_brick_stairs', facing='west', half='bottom', shape='straight') for x in range(11, 15): self.put(x, 26, 37, 'minecraft:stone_brick_stairs', facing='north', half='bottom', shape='straight') # Railings remain outside all stair arrival and departure cells. for x in range(10, 26): self.put(x, 16, 51, 'minecraft:oak_fence') for z in range(48, 51): for x in (10, 25): self.put(x, 16, z, 'minecraft:oak_fence') for x in range(16, 26): self.put(x, 16, 48, 'minecraft:oak_fence') for x, z, y in [(10, 51, 16), (25, 51, 16), (25, 48, 16), (43, 43, 12), (49, 40, 12), (29, 43, 22), (29, 40, 22), (10, 34, 27), (20, 36, 27)]: self.post_light(x, y, z) def post_light(self, x: int, y: int, z: int) -> None:
#10 · 2026-09-24T21:47:25.231Z · exit=0 · 0s · cp generate.py blocks.py
cp generate.py blocks.py cp /opt/tools/template_render.mjs render.mjs mkdir -p lodge test samples previews

记录中的输出末尾

#11 · 2026-09-24T21:47:25.297Z · exit=0 · 0s · cat > lodge/plan.py <<'PY'
cat > lodge/plan.py <<'PY' # Resolve a constrained lodge grammar into connected, dimensioned volumes. from __future__ import annotations from dataclasses import asdict, dataclass, field import random from typing import Any DOMAINS = { 'topology': ('single', 'ell', 'twin', 'courtyard', 'staggered'), 'width': (15, 17, 19), 'depth': (17, 19, 21), 'floors': (1, 2, 3), 'ridge': ('z', 'x'), 'profile': ('chalet', 'steep', 'bell'), 'wing_size': (9, 11, 13), 'wing_floors': (1, 2), 'wing_ridge': ('parallel', 'cross'), 'placement': ('front', 'middle', 'rear'), 'tower': ('none', 'stone', 'timber'), 'tower_extra': (1, 2), 'tower_width': (9, 11), 'conservatory': (False, True), 'porch': ('entry', 'full', 'wrap'), 'balcony': (False, True), 'dormers': (0, 1, 2), 'terrain': ('garden', 'terrace', 'cliff'), 'approach': ('straight', 'landing'), 'entrance': ('center', 'left', 'right'), 'layout': ('open', 'cross', 'suites'), 'stair_side': ('left', 'right'), 'palette': ('honey', 'ivory', 'ochre'), 'trees': (3, 4, 5), } LIMITS = {'max_axis': 112, 'max_height': 96, 'max_volume': 900000, 'floor_height': 6, 'tree_height': (15, 24)} DEFAULT_SEED = 20260924 @dataclass class Volume: """A connected room stack, with a roof in local cross-ridge coordinates.""" name: str x0: int z0: int x1: int z1: int base: int floors: int axis: str = 'z' profile: str = 'chalet' kind: str = 'house' attic: bool = False parent: str | None = None floor_step: int = 6 @property def levels(self): return [self.base + i * self.floor_step for i in range(self.floors + int(self.attic))] @property def uc(self): return (self.x0 + self.x1) // 2 if self.axis == 'z' else (self.z0 + self.z1) // 2 @property def u0(self): return self.x0 if self.axis == 'z' else self.z0 @property def u1(self): return self.x1 if self.axis == 'z' else self.z1 @property def v0(self): return self.z0 if self.axis == 'z' else self.x0 @property def v1(self): return self.z1 if self.axis == 'z' else self.x1 def xyz(self, u, y, v): return (u, y, v) if self.axis == 'z' else (v, y, u) def uv(self, x, z): return (x, z) if self.axis == 'z' else (z, x) def contains(self, x, z, margin=0): return self.x0 - margin <= x <= self.x1 + margin and self.z0 - margin <= z <= self.z1 + margin def roof(self, x, z): u, _ = self.uv(x, z) d = (self.u1 - self.u0) // 2 - abs(u - self.uc) eave = self.levels[-1] + (2 if self.attic else 5) if self.kind == 'glass': return eave + min(x - self.x0, self.x1 - x, z - self.z0, self.z1 - z, 3) if self.kind in ('stone', 'timber'): d = min(x - self.x0, self.x1 - x, z - self.z0, self.z1 - z) eave += 1 rise = d if self.profile == 'chalet' or d < 0 else 2 * d if self.profile == 'steep' else d + max(0, d - 2) return eave + rise def interior(self, x, y, z): return self.x0 < x < self.x1 and self.z0 < z < self.z1 and self.base < y < self.roof(x, z) @dataclass class Plan: seed: int parameters: dict volumes: list[Volume] links: list[dict] entrance: tuple porch: tuple terrace_front: int approach: list[dict] tree_sites: list[tuple] origin: tuple size_xyz: tuple limits: dict def record(self): return asdict(self) def signature(self): """Describe actual relative massing and roof structure, independent of seed.""" m = self.volumes[0] return tuple((v.x0 - m.x0, v.z0 - m.z0, v.x1 - v.x0, v.z1 - v.z0, tuple(y - m.base for y in v.levels), v.axis, v.profile, v.kind) for v in self.volumes) def resolve(seed: int, overrides: dict | None = None, limits: dict | None = None) -> Plan: """Resolve one seed and optional architectural constraints without retries. Args: seed (int): Any signed 64-bit integer. overrides (dict): Explicit values from DOMAINS. limits (dict): Resource limits, constrained to the documented keys. Returns: Plan: Dimensioned, connected design with computed export bounds. """ if type(seed) is not int or not -(2**63) <= seed < 2**63: raise ValueError('seed must be a signed 64-bit integer') overrides = dict(overrides or {}) for key, value in overrides.items(): if key not in DOMAINS or value not in DOMAINS[key] or type(value) is not type(DOMAINS[key][0]): raise ValueError(f'invalid {key}={value!r}; domain: {DOMAINS.get(key, "unknown parameter")}') lim = dict(LIMITS) if limits: if set(limits) - set(LIMITS): raise ValueError('unknown resource limit') lim.update(limits) if lim['floor_height'] != 6: raise ValueError('floor_height is fixed at 6 for the stair components') rng = random.Random(seed) p = {k: rng.choice(v) for k, v in DOMAINS.items()} p.update(overrides) def condition(key, allowed, reason): if p[key] not in allowed: if key in overrides: raise ValueError(f'{key}={p[key]!r}: {reason}; allowed {allowed}') p[key] = rng.choice(tuple(allowed)) condition('wing_floors', range(1, min(2, p['floors']) + 1), 'wings cannot exceed the main full-storey count') if p['floors'] == 1: condition('balcony', (False,), 'balconies require a full upper storey') if p['topology'] in ('twin', 'courtyard'): condition('porch', ('entry', 'full'), 'side wings occupy the wraparound interface') base = {'garden': 4, 'terrace': 7, 'cliff': 16}[p['terrain']] w, d = p['width'], p['depth'] volumes = [Volume('main', 0, 0, w - 1, d - 1, base, p['floors'], p['ridge'], p['profile'], attic=True)] links = [] def add(name, host, side, width, length, alignment, floors, axis, kind='house', profile=None): if side in ('left', 'right'): z0 = alignment x0 = host.x0 - width + 1 if side == 'left' else host.x1 v = Volume(name, x0, z0, x0 + width - 1, z0 + length - 1, base, floors, axis, profile or p['profile'], kind, parent=host.name) z = (max(host.z0, v.z0) + min(host.z1, v.z1)) // 2 x = host.x0 if side == 'left' else host.x1 links.append({'a': host.name, 'b': name, 'x': x, 'z': z, 'axis': 'x', 'floor': base}) else: x0 = alignment v = Volume(name, x0, host.z1, x0 + width - 1, host.z1 + length - 1, base, floors, axis, profile or p['profile'], kind, parent=host.name) x = (max(host.x0, v.x0) + min(host.x1, v.x1)) // 2 links.append({'a': host.name, 'b': name, 'x': x, 'z': host.z1, 'axis': 'z', 'floor': base}) volumes.append(v) return v main = volumes[0] wing_axis = p['ridge'] if p['wing_ridge'] == 'parallel' else ('x' if p['ridge'] == 'z' else 'z') count = {'single': 0, 'ell': 1, 'twin': 2, 'courtyard': 2, 'staggered': 1}[p['topology']] first_side = rng.choice(('left', 'right')) sides = [first_side] if count == 1 else ['left', 'right'] if count == 2 else [] for i, side in enumerate(sides): width = p['wing_size'] length = rng.choice((11, 13, 15)) z0 = {'front': -2, 'middle': (d - length) // 2, 'rear': d - length + 2}[p['placement']] if p['topology'] == 'courtyard': z0, length = -rng.choice((5, 7)), d - 3 elif i: z0 += rng.choice((-2, 2)) add('wing_' + side, main, side, width, length, z0, p['wing_floors'], wing_axis) rear_host = main if p['topology'] == 'staggered': rear_host = add('rear_wing', main, 'rear', p['wing_size'], rng.choice((9, 11, 13)), (w - p['wing_size']) // 2, p['wing_floors'], 'z') if p['tower'] != 'none': tw = p['tower_width'] add('tower', rear_host, 'rear', tw, tw, (rear_host.x0 + rear_host.x1 - tw + 1) // 2, min(5, p['floors'] + p['tower_extra']), 'z', p['tower'], 'bell' if p['tower'] == 'timber' else 'chalet') if p['conservatory']: host = next((v for v in volumes if v.name == 'wing_right'), main) length = min(11, host.z1 - host.z0 + 1) add('conservatory', host, 'right', rng.choice((7, 9)), length, (host.z0 + host.z1 - length + 1) // 2, 1, 'z', 'glass', 'chalet') ex = w // 2 + {'center': 0, 'left': -3, 'right': 3}[p['entrance']] porch_width = 7 if p['porch'] == 'entry' else w porch_x0 = max(0, min(ex - porch_width // 2, w - porch_width)) porch = (porch_x0, -4, porch_x0 + porch_width - 1, -1) front = min(v.z0 for v in volumes) - 6 approach = [] rises = base - 1 landing_length = 3 if p['approach'] == 'landing' else 0 startz = front - rises - landing_length - 1 y, z = 1, startz approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'landing'}) for i in range(rises): if i == rises // 2 and landing_length: for _ in range(landing_length): z += 1 approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'landing'}) y, z = y + 1, z + 1 approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'step'}) for zz in range(z + 1, 1): approach.append({'x': ex, 'z': zz, 'y': base, 'kind': 'landing'}) # Dormers attach to the main attic, far enough inside the wall to open into it. for i in range(p['dormers']): side = -1 if i == 0 else 1 vc = main.v0 + 5 if i == 0 else main.v1 - 5 ua, ub = (main.u0 - 1, main.uc) if side < 0 else (main.uc, main.u1 + 1) a, _, b = main.xyz(ua, 0, vc - 2) c, _, e = main.xyz(ub, 0, vc + 2) volumes.append(Volume(f'dormer_{i}', a, b, c, e, main.levels[-1], 1, 'x' if main.axis == 'z' else 'z', 'chalet', 'dormer', parent='main')) xmin = min(v.x0 for v in volumes) - 7 xmax = max(v.x1 for v in volumes) + 7 zmin = startz - 3 zmax = max(v.z1 for v in volumes) + 7 sites = [(xmin + 2, max(1, main.z1 - 2)), (xmax - 2, main.z1 + 2), ((xmin + xmax) // 2 - 6, zmax - 2), (xmin + 3, main.z0 - 1), (xmax - 2, main.z0 + 2)] trees = [(x, z, rng.randint(*lim['tree_height'])) for x, z in sites[:p['trees']]] maxroof = max(v.roof((v.x0 + v.x1) // 2, (v.z0 + v.z1) // 2) for v in volumes) height = max(maxroof + 7, base + lim['tree_height'][1] + 3) origin = (2 - xmin, 0, 2 - zmin) size = (xmax - xmin + 5, height + 1, zmax - zmin + 5) if max(size[0], size[2]) > lim['max_axis'] or size[1] > lim['max_height'] or size[0] * size[1] * size[2] > lim['max_volume']: raise ValueError(f'design bounds {size} exceed resource limits {lim}') return Plan(seed, p, volumes, links, (ex, base + 1, 0), porch, front, approach, trees, origin, size, lim) PY

记录中的输出末尾

#12 · 2026-09-24T21:52:28.477Z · exit=0 · 0s · cat > lodge/geometry.py <<'PY'
cat > lodge/geometry.py <<'PY' # Compile the lodge plan into sealed shells, circulation, furnishings, and landscape. from __future__ import annotations import json import math import random from pathlib import Path from functools import lru_cache from .plan import Plan, Volume STATE_FILE = Path('/work/generator/MCRender/resources/1.21.1-blocks.json') STATES = json.loads(STATE_FILE.read_text()) STONE = ('stone_bricks',) * 8 + ('andesite', 'cobblestone', 'mossy_stone_bricks') ROCK = ('stone',) * 9 + ('andesite', 'tuff', 'cobblestone') PAVING = ('stone_bricks',) * 9 + ('andesite', 'mossy_stone_bricks', 'cobblestone') ROOF = ('deepslate_tiles',) * 12 + ('deepslate_bricks', 'cobbled_deepslate') PALETTES = { 'honey': ('smooth_sandstone', 'stripped_spruce_log', 'spruce_planks', 'dark_oak_log', 'yellow_stained_glass', 'orange'), 'ivory': ('calcite', 'stripped_dark_oak_log', 'spruce_planks', 'dark_oak_log', 'white_stained_glass', 'green'), 'ochre': ('smooth_sandstone', 'stripped_oak_log', 'oak_planks', 'spruce_log', 'yellow_stained_glass', 'red'), } DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)} @lru_cache(None) def blockstate(name, properties=()): """Validate and canonicalize a vanilla state with explicit default properties.""" name = name.removeprefix('minecraft:') if name not in STATES: raise ValueError(f'not a Java 1.21.1 block: {name}') domains, defaults = STATES[name] values = dict(defaults) for key, value in properties: if key not in domains or str(value) not in domains[key]: raise ValueError(f'invalid {name}[{key}={value}]') values[key] = str(value) return 'minecraft:' + name + ('[' + ','.join(f'{k}={v}' for k, v in sorted(values.items())) + ']' if values else '') def name_of(state): return (state or 'minecraft:air').split('[')[0].removeprefix('minecraft:') class Scene: """Sparse local-coordinate scene with shared spatial reservations and evidence.""" def __init__(self, plan: Plan): self.plan = plan self.rng = random.Random(plan.seed) self.blocks = {} self.reserved = set() self.walks = set() self.windows = [] self.doors = [] self.beds = [] self.stairs = [] self.ladders = [] self.rooms = [] self.lights = [] self.attachments = [] self.enclosed = [] self.ground = {} self.furniture = [] self.plaster, self.timber, self.floor, self.beam, self.glass, self.color = PALETTES[plan.parameters['palette']] def p(self, x, y, z, name, **props): self.blocks[x, y, z] = blockstate(name, tuple(sorted(props.items()))) def get(self, x, y, z): return name_of(self.blocks.get((x, y, z))) def box(self, bounds, name, **props): 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.p(x, y, z, name, **props) def mix(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.p(x, y, z, self.rng.choice(palette)) def clear(self, bounds): self.box(bounds, 'air') def reserve(self, point, headroom=2): x, y, z = point self.walks.add(point) for dy in range(headroom): self.reserved.add((x, y + dy, z)) def lamp(self, x, y, z, hanging=False): self.p(x, y, z, 'lantern', hanging=str(hanging).lower()) self.lights.append((x, y, z)) self.attachments.append(((x, y, z), (x, y + (1 if hanging else -1), z))) def door(self, x, y, z, facing): for half, dy in (('lower', 0), ('upper', 1)): self.p(x, y + dy, z, 'spruce_door', half=half, facing=facing) self.doors.append((x, y, z)) dx, dz = DIRECTIONS[facing] for side in (-1, 1): self.reserve((x + side * dx, y, z + side * dz)) def stair(self, x, y, z, facing, material='spruce', walking=False): self.p(x, y, z, material + '_stairs', facing=facing) if walking: self.stairs.append((x, y, z, facing)) self.reserve((x, y + 1, z), 3) def occupied_elsewhere(self, volume, x, y, z): return any(v is not volume and v.interior(x, y, z) for v in self.plan.volumes) def route_reservations(self): """Reserve main axes, portals, furniture access, stairs, and ladder landings.""" p = self.plan for v in p.volumes: if v.kind == 'dormer': continue for floor in v.levels: for u in range(v.uc - 1, v.uc + 2): for vv in range(v.v0 + 1, v.v1): x, y, z = v.xyz(u, floor + 1, vv) if y + 1 < v.roof(x, z): self.reserve((x, y, z)) # A crossing axis keeps side-wing interfaces connected. vv = (v.v0 + v.v1) // 2 for u in range(v.u0 + 1, v.u1): self.reserve(v.xyz(u, floor + 1, vv)) if v.name == 'main': sign = -1 if p.parameters['stair_side'] == 'left' else 1 us = (v.uc + sign * 2, v.uc + sign * 3) for floor in v.levels[:-1]: for i in range(6): vv = v.v1 - 8 + i for u in us: x, y, z = v.xyz(u, floor + i + 1, vv) for yy in range(floor + 1, floor + 10): self.reserved.add((x, yy, z)) self.reserve((x, y + 1, z), 3) for u in range(min(us) - 1, max(us) + 2): for vv in (v.v1 - 9, v.v1 - 2, v.v1 - 1): self.reserve(v.xyz(u, floor + 1, vv)) self.reserve(v.xyz(u, floor + 7, vv)) elif len(v.levels) > 1: x, _, z = v.xyz(v.uc, 0, v.v1 - 1) for y in range(v.base + 1, v.levels[-1] + 3): self.reserved.add((x, y, z)) for link in p.links: x, z, f = link['x'], link['z'], link['floor'] for dx in range(-2, 3): for dz in range(-2, 3): self.reserve((x + dx, f + 1, z + dz), 3) ex, ey, ez = p.entrance for x in range(ex - 1, ex + 2): for z in range(ez - 4, ez + 4): self.reserve((x, ey, z), 3) if p.parameters['balcony']: for x in range(ex - 1, ex + 2): for z in range(-3, 4): self.reserve((x, ey + 6, z), 3) def terrain(self): """Grow solid crag columns and an irregular garden island around the plan.""" plan = self.plan base = plan.volumes[0].base ox, _, oz = plan.origin xmin, xmax = 2 - ox, plan.size_xyz[0] - ox - 3 zmin, zmax = 2 - oz, plan.size_xyz[2] - oz - 3 masses = [v for v in plan.volumes if v.kind != 'dormer'] cx = (xmin + xmax) / 2 cz = (zmin + zmax) / 2 for x in range(xmin, xmax + 1): for z in range(zmin, zmax + 1): corner = max(0, xmin + 4 - x, x - xmax + 4) + max(0, zmin + 4 - z, z - zmax + 4) if corner > 4 + self.rng.randrange(2): continue dist = min(max(v.x0 - x, 0, x - v.x1) + max(v.z0 - z, 0, z - v.z1) for v in masses) h = 1 if dist < 6 and z >= plan.terrace_front: h = max(1, base - 1 - max(0, dist - 2) * (3 if base > 10 else 1)) if dist <= 2: h = base if plan.porch[0] - 2 <= x <= plan.porch[2] + 2 and plan.terrace_front <= z < 1: h = base if x in (xmin, xmax) or z in (zmin, zmax): h = 1 seam = self.rng.choice(('stone', 'stone', 'andesite', 'tuff')) for y in range(h): self.p(x, y, z, self.rng.choice((seam,) * 15 + ROCK) if base > 10 or y < h - 2 else 'dirt') self.p(x, h, z, self.rng.choice(('grass_block',) * 6 + ('moss_block', 'coarse_dirt'))) self.ground[x, z] = h # Foundations, walks, retaining edges: all connected to terrain at y=0. for v in masses: self.mix((v.x0, 0, v.z0, v.x1, base - 1, v.z1), STONE) for x in range(plan.porch[0] - 1, plan.porch[2] + 2): for z in range(plan.terrace_front, 0): self.mix((x, 0, z, x, base, z), STONE) self.p(x, base, z, self.rng.choice(PAVING)) self.ground[x, z] = base for x, z in list(self.ground): h = self.ground[x, z] if h == base and not any(v.contains(x, z) for v in masses): self.p(x, h, z, self.rng.choice(PAVING)) edge = any(self.ground.get((x + dx, z + dz), -1) < base - 1 for dx, dz in DIRECTIONS.values()) if edge and abs(x - plan.entrance[0]) > 3: self.p(x, h + 1, z, 'cobblestone_wall') if (x + z) % 6 == 0: self.p(x, h + 1, z, 'stone_bricks') self.p(x, h + 2, z, 'stone_brick_slab') for patch in plan.approach: x, z, y = patch['x'], patch['z'], patch['y'] for xx in range(x - 2, x + 3): self.mix((xx, 0, z, xx, y - 1, z), STONE) self.clear((xx, y, z, xx, max(base + 2, y + 3), z)) if patch['kind'] == 'step': self.stair(xx, y, z, 'south', 'stone_brick', True) else: self.p(xx, y, z, self.rng.choice(PAVING)) self.reserve((xx, y + 1, z), 3) self.ground[xx, z] = y if z < plan.terrace_front: for xx in (x - 3, x + 3): self.mix((xx, 0, z, xx, y, z), STONE) self.p(xx, y + 1, z, 'stone_brick_slab') if (z - plan.approach[0]['z']) % 5 == 0: self.p(xx, y + 1, z, 'stone_bricks') self.lamp(xx, y + 2, z) def shells(self): """Union room envelopes before placing roof planes and real timber frames.""" for v in self.plan.volumes: for f in v.levels: self.box((v.x0, f, v.z0, v.x1, f, v.z1), self.floor if v.kind != 'glass' else 'polished_andesite') for x in range(v.x0, v.x1 + 1): for z in range(v.z0, v.z1 + 1): if x not in (v.x0, v.x1) and z not in (v.z0, v.z1): continue u, vv = v.uv(x, z) top = v.roof(x, z) for y in range(v.base + 1, top): if self.occupied_elsewhere(v, x, y, z): continue frame = x in (v.x0, v.x1) and z in (v.z0, v.z1) frame |= ((x - v.x0) % 4 == 0 and z in (v.z0, v.z1)) or ((z - v.z0) % 4 == 0 and x in (v.x0, v.x1)) belt = y in v.levels or y == v.levels[-1] + (1 if v.attic else 4) name = self.timber if frame else self.beam if belt else 'glass' if v.kind == 'glass' else self.rng.choice(STONE) if v.kind == 'stone' else self.plaster self.p(x, y, z, name, **({'axis': 'y' if frame else 'x' if z in (v.z0, v.z1) else 'z'} if name.endswith('log') else {})) if v.kind not in ('glass', 'stone', 'timber'): # Timber follows both gable verges below the slate. if vv in (v.v0, v.v1) and not self.occupied_elsewhere(v, x, top - 1, z): self.p(x, top - 1, z, self.timber, axis=v.axis) for f in v.levels: for x in range(v.x0, v.x1 + 1): for z in (v.z0, v.z1): if not self.occupied_elsewhere(v, x, f, z): self.p(x, f, z, self.beam, axis='x') for z in range(v.z0, v.z1 + 1): for x in (v.x0, v.x1): if not self.occupied_elsewhere(v, x, f, z): self.p(x, f, z, self.beam, axis='z') for v in self.plan.volumes: overhang = 1 if v.kind == 'glass' else 2 for x in range(v.x0 - overhang, v.x1 + overhang + 1): for z in range(v.z0 - overhang, v.z1 + overhang + 1): h = v.roof(x, z) u, vv = v.uv(x, z) toward = 'east' if u < v.uc else 'west' if v.axis == 'x': toward = 'south' if u < v.uc else 'north' if v.kind in ('stone', 'timber', 'glass'): ds = [(x - v.x0, 'east'), (v.x1 - x, 'west'), (z - v.z0, 'south'), (v.z1 - z, 'north')] toward = min(ds)[1] dx, dz = DIRECTIONS[toward] low = min(h, v.roof(x - dx, z - dz)) for y in range(low, h + 1): if not self.occupied_elsewhere(v, x, y, z): self.p(x, y, z, 'glass' if v.kind == 'glass' else self.rng.choice(ROOF)) if v.kind != 'glass' and not self.occupied_elsewhere(v, x, h + 1, z): if u == v.uc and v.kind not in ('stone', 'timber'): self.p(x, h + 1, z, 'deepslate_tile_slab') else: edge = x in (v.x0 - overhang, v.x1 + overhang) or z in (v.z0 - overhang, v.z1 + overhang) self.stair(x, h + 1, z, toward, 'cobbled_deepslate' if edge else 'deepslate_tile') # Carved ridge ends, anchored into the roof cap. if v.kind not in ('glass', 'stone', 'timber'): for vv in (v.v0 - 2, v.v1 + 2): x, _, z = v.xyz(v.uc, 0, vv) h = v.roof(x, z) if not self.occupied_elsewhere(v, x, h + 2, z): self.p(x, h + 1, z, self.timber, axis=v.axis) self.p(x, h + 2, z, 'oak_fence') if v.name == 'main' and self.plan.parameters['profile'] != 'chalet': self.p(x, h + 3, z, 'lightning_rod') def opening(self, x, f, z, face, width=2): """Glaze an exterior bay only when it has clear indoor and outdoor space.""" dx, dz = DIRECTIONS[face] cells = [(x + (i if dz else 0), y, z + (i if dx else 0)) for i in range(width) for y in (f + 2, f + 3)] inside = [(a - dx, b, c - dz) for a, b, c in cells] outside = [(a + dx, b, c + dz) for a, b, c in cells] if any(c in self.reserved for c in cells + inside): return if any(self.get(*q) not in ('air',) for q in inside + outside): return if any(self.get(*q) in ('air', 'glass') for q in cells): return for q in cells: self.p(*q, self.glass) self.windows.append({'cells': cells, 'inside': inside, 'outside': outside}) self.reserved.update(inside + outside + cells) for i in range(width): xx, zz = x + (i if dz else 0), z + (i if dx else 0) self.p(xx + dx, f + 1, zz + dz, 'spruce_trapdoor', facing=face, half='top') self.attachments.append(((xx + dx, f + 1, zz + dz), (xx, f + 1, zz))) # Compact shutters anchor to wall blocks, never cover the glazing. for i in (-1, width): xx, zz = x + (i if dz else 0), z + (i if dx else 0) for y in (f + 2, f + 3): q = (xx + dx, y, zz + dz) if self.get(xx, y, zz) != 'air' and self.get(*q) == 'air' and q not in self.reserved: self.p(*q, 'spruce_trapdoor', facing=face, open='true') self.attachments.append((q, (xx, y, zz))) def openings(self): for v in self.plan.volumes: if v.kind == 'glass': continue for f in v.levels: for face, z in (('north', v.z0), ('south', v.z1)): for x in range(v.x0 + 2, v.x1 - 1, 4): self.opening(x, f, z, face) for face, x in (('west', v.x0), ('east', v.x1)): for z in range(v.z0 + 2, v.z1 - 1, 4): self.opening(x, f, z, face) if v.attic: for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')): x, f, z = v.xyz(v.uc - 1, v.levels[-1], vv) self.opening(x, f + 1, z, face, 3) for link in self.plan.links: x, z, f = link['x'], link['z'], link['floor'] if link['axis'] == 'x': self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1)) self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor) else: self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1)) self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor) x, y, z = self.plan.entrance self.clear((x, y, z - 1, x, y + 2, z + 1)) self.door(x, y, z, 'north') for xx in (x - 1, x + 1): self.box((xx, y, z - 1, xx, y + 2, z - 1), self.timber, axis='y') self.box((x - 1, y + 3, z - 1, x + 1, y + 3, z - 1), self.beam, axis='x') def circulation(self): """Cut stacked stairs and supported ladders after all intersecting roofs.""" for v in self.plan.volumes: if v.name == 'main': sign = -1 if self.plan.parameters['stair_side'] == 'left' else 1 for f in v.levels[:-1]: for i in range(6): for u in (v.uc + 2 * sign, v.uc + 3 * sign): x, y, z = v.xyz(u, f + i + 1, v.v1 - 8 + i) self.clear((x, y + 1, z, x, y + 3, z)) self.p(x, y - 1, z, self.floor) self.stair(x, y, z, 'south' if v.axis == 'z' else 'east', walking=True) for u in (v.uc + 2 * sign, v.uc + 3 * sign): for vv in (v.v1 - 2, v.v1 - 1): x, _, z = v.xyz(u, 0, vv) self.p(x, f + 6, z, self.floor) self.clear((x, f + 7, z, x, f + 9, z)) # Outer guard on each stair opening, leaving return aisles clear. for f in v.levels[1:]: for vv in range(v.v1 - 7, v.v1 - 2): u = v.uc + 4 * sign x, y, z = v.xyz(u, f + 1, vv) if self.get(x, f, z) != 'air' and (x, y, z) not in self.reserved and y + 2 < v.roof(x, z): self.p(x, y, z, 'spruce_fence') elif len(v.levels) > 1: x, _, z = v.xyz(v.uc, 0, v.v1 - 1) bx, _, bz = v.xyz(v.uc, 0, v.v1) self.box((bx, v.base + 1, bz, bx, v.levels[-1] + 3, bz), self.beam, axis='y') for y in range(v.base + 1, v.levels[-1] + 3): self.p(x, y, z, 'ladder', facing='north' if v.axis == 'z' else 'west') self.ladders.append((x, y, z)) for f in v.levels: u = v.uc - 1 xx, yy, zz = v.xyz(u, f + 1, v.v1 - 1) self.clear((xx, yy, zz, xx, yy + 1, zz)) self.reserve((xx, yy, zz)) def room(self, v, f, purpose, bounds, target): self.rooms.append({'volume': v.name, 'floor': f, 'purpose': purpose, 'bounds': bounds, 'target': target, 'furniture': [], 'lights': []}) self.reserve(target) return self.rooms[-1] def furnish_cell(self, room, x, y, z, name, **props): if (x, y, z) in self.reserved or self.get(x, y, z) != 'air' or self.get(x, y - 1, z) == 'air': return False self.p(x, y, z, name, **props) room['furniture'].append((x, y, z)) self.furniture.append((x, y, z)) return True def bed(self, room, x, y, z, facing): dx, dz = DIRECTIONS[facing] pts = [(x, y, z), (x + dx, y, z + dz)] if any(q in self.reserved or self.get(*q) != 'air' or self.get(q[0], y - 1, q[2]) == 'air' for q in pts): return False for q, part in zip(pts, ('foot', 'head')): self.p(*q, self.color + '_bed', facing=facing, part=part) room['furniture'].append(q) self.beds.append(pts[0]) return True def interiors(self): """Allocate functions to actual rooms, with layouts responding to the plan.""" for v in self.plan.volumes: if v.kind == 'dormer': # Its floor continues the furnished attic, with a useful window bench. continue for level, f in enumerate(v.levels): layout = self.plan.parameters['layout'] vc = (v.v0 + v.v1) // 2 segments = [(v.v0 + 1, v.v1 - 1)] if v.name == 'main' and level < v.floors and layout != 'open': segments = [(v.v0 + 1, vc - 1), (vc + 1, v.v1 - 1)] for u in range(v.u0 + 1, v.u1): for y in range(f + 1, f + 6): x, _, z = v.xyz(u, y, vc) if abs(u - v.uc) <= 1 or (x, y, z) in self.reserved: continue self.p(x, y, z, self.timber if u % 4 == 0 else self.plaster, **({'axis': 'y'} if u % 4 == 0 else {})) # A framed three-wide arch leaves the central hall continuous. for u in range(v.uc - 1, v.uc + 2): x, _, z = v.xyz(u, 0, vc) self.p(x, f + 4, z, self.beam, axis='x' if v.axis == 'z' else 'z') for index, (va, vb) in enumerate(segments): if v.kind == 'glass': purpose = 'winter garden and herbal workroom' elif v.name == 'main': purpose = ('hearth kitchen and dining', 'parlour and pantry')[index] if level == 0 else 'bedchamber and study' if level == len(v.levels) - 1 else ('guest suite' if layout == 'suites' else 'library and workroom') elif v.name == 'tower': purpose = ('pantry', 'map room', 'watchkeeper bedroom', 'archive', 'lookout study')[min(level, 4)] else: purpose = 'guest bedroom' if level else self.rng.choice(('craft workshop', 'dining parlour', 'library')) ua, ub = v.u0 + 1, v.u1 - 1 target = v.xyz(v.uc, f + 1, (va + vb) // 2) x0, _, z0 = v.xyz(ua, 0, va) x1, _, z1 = v.xyz(ub, 0, vb) room = self.room(v, f, purpose, (x0, f + 1, z0, x1, f + 4, z1), target) # Furnishing banks on both sides of the protected longitudinal hall. spots = [] for u in (ua + 1, ub - 1): for vv in range(va + 1, vb, 2): x, y, z = v.xyz(u, f + 1, vv) if y + 3 < v.roof(x, z) and all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)): spots.append((x, y, z)) # The minimum room width always admits perimeter furniture, even at ladders. if not spots: for x in range(v.x0 + 1, v.x1): for z in range(v.z0 + 1, v.z1): y = f + 1 if all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)) and self.get(x, f, z) != 'air': spots.append((x, y, z)) if not spots: raise AssertionError(f'no furnishing positions in {v.name} {f}') # Every room has a low supported lamp and a second task light when large. for lamp_index in sorted(set((0, len(spots) - 1))): x, y, z = spots[lamp_index] self.p(x, y, z, 'barrel', facing='up') room['furniture'].append((x, y, z)) self.lamp(x, y + 1, z) room['lights'].append((x, y + 1, z)) candidates = spots[1:-1] or spots[1:] wants_bed = 'bed' in purpose or 'suite' in purpose if wants_bed: found = False for x, y, z in candidates: if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'): found = True break if not found: for u in range(ua, ub + 1): for vv in range(va, vb): x, y, z = v.xyz(u, f + 1, vv) if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'): found = True break if found: break assert found, f'bed has no valid location in {v.name} {f}' furnishings = ('smoker', 'crafting_table', 'barrel', 'furnace', 'bookshelf') if 'kitchen' in purpose else ('composter', 'moss_block', 'barrel', 'crafting_table') if v.kind == 'glass' else ('bookshelf', 'cartography_table', 'barrel', 'loom', 'chest') for i, (x, y, z) in enumerate(candidates): name = furnishings[i % len(furnishings)] props = {'facing': 'south'} if name in ('smoker', 'furnace', 'chest', 'loom', 'barrel') else {} if name in ('smoker', 'furnace'): props['lit'] = 'true' if self.furnish_cell(room, x, y, z, name, **props): if i % 2 == 0 and (x, y + 1, z) not in self.reserved: if name == 'bookshelf' and self.get(x, y + 2, z) == 'air': self.p(x, y + 1, z, 'bookshelf') else: self.p(x, y + 1, z, 'potted_fern' if v.kind != 'glass' else self.rng.choice(('potted_azalea_bush', 'potted_poppy', 'potted_dandelion'))) self.attachments.append(((x, y + 1, z), (x, y, z))) # A table and facing seat fill a front-side bay without covering the hall. for offset in (-3, 3): x, y, z = v.xyz(v.uc + offset, f + 1, va + 1) if self.furnish_cell(room, x, y, z, self.floor): if (x, y + 1, z) not in self.reserved and self.get(x, y + 1, z) == 'air': self.p(x, y + 1, z, 'potted_spruce_sapling') self.attachments.append(((x, y + 1, z), (x, y, z))) sx, sy, sz = v.xyz(v.uc + offset, f + 1, va + 2) self.furnish_cell(room, sx, sy, sz, 'spruce_stairs', facing='north' if v.axis == 'z' else 'west') # Wool inlays provide colour without reducing headroom. for u in range(v.uc - 1, v.uc + 2): for vv in range(va + 2, min(vb, va + 5)): x, _, z = v.xyz(u, 0, vv) if self.get(x, f, z).endswith('planks'): self.p(x, f, z, self.color + '_wool') self.enclosed.append(target) PY

记录中的输出末尾

#13 · 2026-09-24T21:55:17.221Z · exit=0 · 0s · cat >> lodge/geometry.py <<'PY'
cat >> lodge/geometry.py <<'PY' def chimney(self): """Run a masonry chimney from a supported hearth through the roof shoulder.""" v = self.plan.volumes[0] x, z, f = v.x0 + 1, v.z1 - 2, v.base h = max(v.roof(x, z), v.roof(x + 1, z)) + 5 self.mix((x, f + 1, z, x + 1, h, z + 1), STONE) for yy in (h - 3, h): self.box((x - 1, yy, z - 1, x + 2, yy, z + 2), 'stone_bricks') self.p(x, h + 1, z, 'campfire', lit='true') for xx, zz in ((x - 1, z - 1), (x + 2, z - 1), (x - 1, z + 2), (x + 2, z + 2)): self.p(xx, h + 1, zz, 'stone_brick_wall') self.box((x - 1, h + 2, z - 1, x + 2, h + 2, z + 2), 'stone_brick_slab') # A contained hearth opens to the room, with a proper masonry hood. for xx in (x, x + 1): self.p(xx, f + 1, z, 'campfire', lit='true') self.p(xx, f + 2, z, 'air') self.p(xx, f + 1, z - 1, 'iron_bars', east='true', west='true') self.p(xx, f + 2, z - 1, 'iron_bars', east='true', west='true') self.box((x, f + 3, z - 1, x + 1, f + 3, z - 1), 'stone_bricks') def porches(self): """Build post-supported canopies, optional balcony, and an open side gallery.""" p = self.plan f = p.volumes[0].base ex = p.entrance[0] x0, z0, x1, z1 = p.porch self.box((x0, f, z0, x1, f, z1), self.floor) for x in range(x0 - 1, x1 + 2): for z in range(z0 - 1, 0): balcony = p.parameters['balcony'] and ex - 3 <= x <= ex + 3 and -3 <= z <= -1 if not balcony: h = f + 5 + (z - z0) // 2 if not any(v.interior(x, h, z) for v in p.volumes): self.p(x, h, z, self.rng.choice(ROOF)) self.p(x, h + 1, z, 'deepslate_tile_slab') posts = {x0, x1} if x1 - x0 > 9: posts.update((ex - 3, ex + 3)) for x in sorted(posts): if abs(x - ex) <= 1: continue self.mix((x, 0, z0, x, f, z0), STONE) self.box((x, f + 1, z0, x, f + 4, z0), self.timber, axis='y') self.p(x, f + 4, z0 - 1, self.floor) self.lamp(x, f + 3, z0 - 1, True) for dx in (-1, 1): if x0 <= x + dx <= x1: self.p(x + dx, f + 4, z0, 'spruce_stairs', facing='east' if dx < 0 else 'west', half='top') self.box((x0, f + 4, z0, x1, f + 4, z0), self.beam, axis='x') for x in range(x0 + 1, x1): if abs(x - ex) > 2: self.p(x, f + 1, z0, 'spruce_fence') if p.parameters['balcony']: self.box((ex - 3, f + 6, -3, ex + 3, f + 6, -1), self.floor) for x in range(ex - 3, ex + 4): self.p(x, f + 7, -3, 'spruce_fence') for x in (ex - 3, ex + 3): self.box((x, f + 1, -3, x, f + 6, -3), self.timber, axis='y') for z in (-2, -1): self.p(x, f + 7, z, 'spruce_fence') self.clear((ex, f + 7, -2, ex, f + 9, 1)) self.door(ex, f + 7, 0, 'north') self.rooms.append({'volume': 'balcony', 'floor': f + 6, 'purpose': 'open balcony', 'bounds': (ex - 2, f + 7, -2, ex + 2, f + 9, -1), 'target': (ex, f + 7, -2), 'furniture': [], 'lights': []}) self.p(ex - 2, f + 7, -1, 'barrel', facing='up') self.lamp(ex - 2, f + 8, -1) self.rooms[-1]['furniture'].append((ex - 2, f + 7, -1)) self.rooms[-1]['lights'].append((ex - 2, f + 8, -1)) if p.parameters['porch'] == 'wrap': main = p.volumes[0] # Choose a free facade interface at planning time. side = p['wrap_side'] if isinstance(p, dict) else p.parameters['wrap_side'] xa, xb = (-4, -1) if side == 'left' else (main.x1 + 1, main.x1 + 4) for x in range(xa, xb + 1): for z in range(-3, main.z1 + 1): self.mix((x, 0, z, x, f - 1, z), STONE) self.p(x, f, z, self.floor) h = f + 5 + (x - xa) // 2 if side == 'left' else f + 5 + (xb - x) // 2 self.p(x, h, z, self.rng.choice(ROOF)) self.p(x, h + 1, z, 'deepslate_tile_slab') outer = xa if side == 'left' else xb for z in range(-2, main.z1 + 1, 5): self.box((outer, f + 1, z, outer, f + 4, z), self.timber, axis='y') self.p(outer, f + 5, z, self.beam, axis='z') self.lamp(outer, f + 3, z + 1, True) self.p(outer, f + 4, z + 1, self.floor) for z in range(0, main.z1): if self.get(outer, f + 1, z) == 'air': self.p(outer, f + 1, z, 'spruce_fence') # Stone tower lookout: a glazed room surrounded by a walkable timber gallery. for v in p.volumes: if v.kind != 'stone': continue top = v.levels[-1] for x in range(v.x0 - 1, v.x1 + 2): for z in range(v.z0 - 1, v.z1 + 2): if v.contains(x, z): continue self.p(x, top, z, self.floor) self.p(x, top + 1, z, 'oak_fence') if x in (v.x0 - 1, v.x1 + 1) and z in (v.z0, v.z1): self.p(x, top - 1, z, 'spruce_stairs', facing='east' if x < v.x0 else 'west', half='top') # East gallery has a two-wide deck so the rail does not block the door. zz = (v.z0 + v.z1) // 2 for z in range(zz - 2, zz + 3): self.p(v.x1 + 2, top, z, self.floor) self.p(v.x1 + 2, top + 1, z, 'oak_fence') self.p(v.x1 + 1, top + 1, z, 'air') self.door(v.x1, top + 1, zz, 'east') self.reserve((v.x1 - 1, top + 1, zz)) def tree(self, x, z, height): base = self.ground[x, z] self.box((x, base + 1, z, x, base + height - 2, z), 'spruce_log', axis='y') for rise in range(4, height, 3): radius = max(1, min(3, (height - rise + 3) // 5)) for dx, dz in DIRECTIONS.values(): for i in range(1, radius): xx, yy, zz = x + dx * i, base + rise, z + dz * i if self.get(xx, yy, zz) == 'air': self.p(xx, yy, zz, 'spruce_log', axis='x' if dx else 'z') for dy in (0, 1): r = max(1, radius - dy) for dx in range(-r, r + 1): for dz in range(-r, r + 1): if abs(dx) + abs(dz) > r + 1 or (abs(dx) == abs(dz) == r and r > 1): continue xx, yy, zz = x + dx, base + rise + dy, z + dz if self.get(xx, yy, zz) == 'air' and (xx, yy, zz) not in self.reserved: self.p(xx, yy, zz, 'spruce_leaves', persistent='true') for yy in range(base + height - 1, base + height + 2): self.p(x, yy, z, 'spruce_leaves', persistent='true') def landscape(self): """Plant grounded conifers and garden borders while preserving every opening.""" for x, z, height in self.plan.tree_sites: self.tree(x, z, height) for (x, z), y in sorted(self.ground.items()): if self.get(x, y, z) not in ('grass_block', 'moss_block', 'coarse_dirt') or self.get(x, y + 1, z) != 'air' or (x, y + 1, z) in self.reserved: continue n = self.rng.random() if n < .14: self.p(x, y + 1, z, self.rng.choice(('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet'))) self.attachments.append(((x, y + 1, z), (x, y, z))) elif n < .20: self.p(x, y + 1, z, self.rng.choice(('azalea_leaves', 'flowering_azalea_leaves')), persistent='true') elif n < .23: self.p(x, y + 1, z, 'mossy_cobblestone') # Vertical ivy grows only against an actual solid corner post. for v in self.plan.volumes: if v.kind in ('dormer', 'glass'): continue for x, z, face in ((v.x0, v.z0 - 1, 'south'), (v.x1 + 1, v.z1, 'west')): dx, dz = DIRECTIONS[face] for y in range(v.base + 1, min(v.levels[-1] + 4, v.base + 13)): if self.get(x, y, z) == 'air' and (x, y, z) not in self.reserved and self.get(x + dx, y, z + dz).endswith('log'): self.p(x, y, z, 'vine', **{face: 'true'}) self.attachments.append(((x, y, z), (x + dx, y, z + dz))) # Entrance heraldry has physical textile blocks, visible without block entity NBT. if self.plan.parameters['floors'] >= 2: v = self.plan.volumes[0] for x in (v.x0 + 1, v.x1 - 1): for y in range(v.base + 8, v.base + 12): z = -1 if (x, y, z) not in self.reserved and self.get(x, y, z) == 'air' and self.get(x, y, 0) != 'air': self.p(x, y, z, self.color + '_wool') self.p(x, v.base + 12, -1, self.beam, axis='z') def connections(self): """Resolve fence arms from final neighboring geometry.""" for (x, y, z), state in list(self.blocks.items()): name = name_of(state) if name.endswith('_fence'): props = {} for face, (dx, dz) in DIRECTIONS.items(): neighbor = self.get(x + dx, y, z + dz) props[face] = 'true' if neighbor.endswith(('_fence', '_log', '_planks')) or neighbor in ('stone_bricks', 'cobblestone') else 'false' self.p(x, y, z, name, **props) def build(self): self.route_reservations() v = self.plan.volumes[0] for x in range(v.x0, v.x0 + 4): for z in range(v.z1 - 3, v.z1 + 2): for y in range(v.base + 1, self.plan.size_xyz[1]): self.reserved.add((x, y, z)) self.terrain() self.shells() self.openings() self.circulation() self.chimney() self.porches() self.interiors() self.landscape() self.connections() return self PY cat > generate.py <<'PY' # Generate reproducible furnished Minecraft Java 1.21.1 lodges from a constrained grammar. from __future__ import annotations import argparse import json from pathlib import Path from blocks import Build as BlockBuild, CONFIG, blockstates_equivalent from lodge.plan import DEFAULT_SEED, DOMAINS, LIMITS, Plan, resolve from lodge.geometry import Scene class Build(BlockBuild): """Compile a resolved architectural plan using the toolset export contract.""" def __init__(self, seed: int = DEFAULT_SEED, parameters: dict | None = None, limits: dict | None = None): self.plan = resolve(seed, parameters, limits) super().__init__(self.plan.size_xyz, seed) self.scene = None def build(self): self.scene = Scene(self.plan).build() ox, oy, oz = self.plan.origin materials = {} for (x, y, z), state in sorted(self.scene.blocks.items()): if state not in materials: materials[state] = self.material(state) self.put(x + ox, y + oy, z + oz, materials[state]) return self def export(self, path: Path): super().export(path) record = self.plan.record() record['rooms'] = self.scene.rooms record['structural_signature'] = self.plan.signature() path.with_suffix('.json').write_text(json.dumps(record, indent=2) + '\n') return path def generate(seed: int = DEFAULT_SEED, parameters: dict | None = None, output: Path | None = None, validate: bool = True, limits: dict | None = None) -> Build: """Compile, validate, and optionally export one reproducible architectural sample. Args: seed (int): Signed 64-bit seed, default 20260924. parameters (dict): Explicit architectural constraints from DOMAINS. output (Path): Optional schematic path, with adjacent resolved JSON. validate (bool): Run geometry checks before export. limits (dict): Optional documented resource limits. Returns: Build: Blocks, resolved plan, and verification evidence. """ build = Build(seed, parameters, limits).build() if validate: from lodge.validation import validate_scene build.validation = validate_scene(build.scene) if output is not None: build.export(Path(output)) build.verify(Path(output)) return build def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--seed', type=int, default=DEFAULT_SEED) parser.add_argument('--output', type=Path, default=Path('output.litematic')) parser.add_argument('--config', type=Path, help='JSON architectural parameter overrides') parser.add_argument('--set', action='append', default=[], metavar='KEY=JSON', help='Override a parameter, e.g. --set floors=2 --set topology=ell') args = parser.parse_args() params = json.loads(args.config.read_text()) if args.config else {} for setting in args.set: key, value = setting.split('=', 1) try: params[key] = json.loads(value) except json.JSONDecodeError: params[key] = value try: build = generate(args.seed, params, args.output) except ValueError as error: parser.error(str(error)) print(json.dumps({'output': str(args.output), 'seed': args.seed, 'bounds': build.plan.size_xyz, 'validation': build.validation}, indent=2)) if __name__ == '__main__': main() PY

记录中的输出末尾

#14 · 2026-09-24T21:57:18.684Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/plan.py' s=open(p).read(); s=s.replace("ex = w // 2 +", "free_sides = [side for side in ('left', 'right') if not any(v.name == 'wing_' + side for v in volumes) and not (side == 'right' and p['conservatory'])]\n if not free_sides:\n condition('porch', ('entry', 'full'), 'no free side for a wraparound gallery')\n p['wrap_side'] = free_sides[0] if free_sides and p['porch'] == 'wrap' else None\n ex = w // 2 +") open(p,'w').write(s) p='blocks.py';s=open(p).read().replace('"name": "build"','"name": "Procedural woodland lodge"').replace('"description": "Generated build"','"description": "Seeded furnished lodge, Java 1.21.1"');open(p,'w').write(s) PY cat > lodge/validation.py <<'PY' # Validate actual final block geometry, physical interfaces, and traversable interiors. from __future__ import annotations from collections import deque, Counter from functools import lru_cache from .geometry import STATES, DIRECTIONS, name_of @lru_cache(None) def parse(state): name = name_of(state) props = {} if state and '[' in state: props = dict(item.split('=') for item in state.split('[', 1)[1][:-1].split(',')) return name, props def walk_passable(name): return name in ('air', 'ladder', 'vine') or name.endswith('_door') def solid_support(name): return name != 'air' and name not in ('ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass') and not name.endswith(('_fence', '_wall', '_door', '_trapdoor', '_slab', '_leaves', '_bed')) and not name.startswith('potted_') def reachable(scene): """Flood walkable standing cells using stair orientation and supported ladders. Args: scene (Scene): Final geometry after every decorative pass. Returns: set: Reachable feet coordinates. Doors are player-operable, closed by default. """ blocks = scene.blocks def at(x, y, z): return name_of(blocks.get((x, y, z))) candidates = set() for (x, y, z), state in blocks.items(): n = name_of(state) if solid_support(n) or n.endswith('_stairs'): candidates.add((x, y + 1, z)) if n == 'ladder': candidates.add((x, y, z)) candidates.add((x, y + 1, z)) nodes = {p for p in candidates if walk_passable(at(*p)) and walk_passable(at(p[0], p[1] + 1, p[2]))} start = scene.plan.approach[0] first = (start['x'], start['y'] + 1, start['z']) assert first in nodes, f'exterior start obstructed {first}: {at(*first)}' seen = {first} queue = deque([first]) while queue: x, y, z = queue.popleft() for face, (dx, dz) in DIRECTIONS.items(): for dy in (0, 1, -1): q = (x + dx, y + dy, z + dz) if q not in nodes or q in seen: continue if dy: stairp = (q[0], q[1] - 1, q[2]) if dy > 0 else (x, y - 1, z) n, props = parse(blocks.get(stairp)) required = face if dy > 0 else next(k for k, d in DIRECTIONS.items() if d == (-dx, -dz)) if not n.endswith('_stairs') or props.get('facing') != required or props.get('half') != 'bottom': continue seen.add(q) queue.append(q) for dy in (-1, 1): q = (x, y + dy, z) if q in nodes and q not in seen and (at(x, y, z) == 'ladder' or at(*q) == 'ladder'): seen.add(q) queue.append(q) return seen def envelope_leaks(scene): """Flood exterior air through porous blocks and detect exposed room targets.""" # Occupied room components share a closed envelope; report the first leaking target. ox, _, oz = scene.plan.origin sx, sy, sz = scene.plan.size_xyz xmin, xmax, zmin, zmax = -ox, sx - ox - 1, -oz, sz - oz - 1 porous = {'air', 'ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass', 'flower_pot', 'lightning_rod'} barriers = set() for p, state in scene.blocks.items(): n = name_of(state) if n in porous or n.endswith(('_stairs', '_slab', '_fence', '_wall', '_leaves', '_bed')) or n.startswith('potted_'): continue barriers.add(p) # Flood from every boundary face, with a compact flat integer visited set. plane = sx * sz def idx(x, y, z): return y * plane + (z + oz) * sx + x + ox blocked = bytearray(sx * sy * sz) for x, y, z in barriers: if xmin <= x <= xmax and zmin <= z <= zmax and 0 <= y < sy: blocked[idx(x, y, z)] = 1 seen = bytearray(len(blocked)) queue = deque() def add(i): if not blocked[i] and not seen[i]: seen[i] = 1 queue.append(i) for y in range(sy): for z in range(sz): for x in (0, sx - 1): add(y * plane + z * sx + x) for x in range(sx): for z in (0, sz - 1): add(y * plane + z * sx + x) for z in range(sz): for x in range(sx): add(z * sx + x) add((sy - 1) * plane + z * sx + x) while queue: i = queue.popleft() y, rem = divmod(i, plane) z, x = divmod(rem, sx) if x: add(i - 1) if x < sx - 1: add(i + 1) if z: add(i - sx) if z < sz - 1: add(i + sx) if y: add(i - plane) if y < sy - 1: add(i + plane) return [p for p in scene.enclosed if seen[idx(*p)]] def validate_scene(scene, envelope=True): """Check final geometry, without trusting the plan's declared room graph.""" blocks = scene.blocks get = scene.get p = scene.plan ox, oy, oz = p.origin sx, sy, sz = p.size_xyz assert all(0 <= x + ox < sx and 0 <= y + oy < sy and 0 <= z + oz < sz for x, y, z in blocks), 'bounds violation' for state in set(blocks.values()): n, props = parse(state) assert n in STATES, state domains, defaults = STATES[n] assert set(props) == set(defaults), state assert all(value in domains[key] for key, value in props.items()), state doors, beds = 0, 0 for (x, y, z), state in blocks.items(): n, props = parse(state) if n.endswith('_door'): other = (x, y + (1 if props['half'] == 'lower' else -1), z) nn, pp = parse(blocks.get(other)) assert n == nn and pp.get('half') != props['half'] and all(props[k] == pp[k] for k in ('facing', 'hinge', 'open', 'powered')), f'broken door {(x,y,z)}' if props['half'] == 'lower': assert solid_support(get(x, y - 1, z)), f'unsupported door {(x,y,z)}' doors += 1 if n.endswith('_bed'): dx, dz = DIRECTIONS[props['facing']] sign = 1 if props['part'] == 'foot' else -1 nn, pp = parse(blocks.get((x + sign * dx, y, z + sign * dz))) assert nn == n and pp.get('part') != props['part'] and pp.get('facing') == props['facing'], f'broken bed {(x,y,z)}' assert solid_support(get(x, y - 1, z)), f'unsupported bed {(x,y,z)}' beds += props['part'] == 'foot' if n == 'ladder': dx, dz = DIRECTIONS[props['facing']] assert solid_support(get(x - dx, y, z - dz)), f'unsupported ladder {(x,y,z)}' if n == 'lantern': dy = 1 if props['hanging'] == 'true' else -1 assert get(x, y + dy, z) not in ('air', 'vine', 'glass'), f'unsupported lantern {(x,y,z)}' if n.startswith('potted_') or n in ('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet'): assert solid_support(get(x, y - 1, z)), f'unsupported plant {(x,y,z)}' if n == 'vine': assert any(props.get(face) == 'true' and solid_support(get(x + dx, y, z + dz)) for face, (dx, dz) in DIRECTIONS.items()), f'unattached vine {(x,y,z)}' for q, support in scene.attachments: assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' for window in scene.windows: assert all('glass' in get(*q) for q in window['cells']), f'overwritten window {window}' assert all(get(*q) == 'air' for q in window['inside'] + window['outside']), f'blocked window {window}' for x, y, z, facing in scene.stairs: n, props = parse(blocks.get((x, y, z))) assert n.endswith('_stairs') and props['facing'] == facing, f'overwritten tread {(x,y,z)}' assert solid_support(get(x, y - 1, z)), f'unsupported tread {(x,y,z)}' assert all(walk_passable(get(x, y + dy, z)) for dy in (1, 2, 3)), f'stair headroom {(x,y,z)}' visited = reachable(scene) for room in scene.rooms: assert room['target'] in visited, f'unreachable {room["volume"]}/{room["purpose"]}: {room["target"]}' assert room['furniture'], f'unfurnished room {room}' assert room['lights'], f'unlit room {room}' assert all(get(*q) == 'lantern' for q in room['lights']), f'overwritten room lights {room}' assert all(abs(q[1] - room['floor']) <= 3 for q in room['lights']), f'light too high {room}' assert beds >= 1, 'no sleeping accommodation' if envelope: leaks = envelope_leaks(scene) assert not leaks, f'weather envelope leaks to {leaks}' return {'rooms': len(scene.rooms), 'beds': beds, 'doors': doors, 'windows': len(scene.windows), 'walking_stairs': len(scene.stairs), 'reachable_cells': len(visited), 'blocks': sum(name_of(s) != 'air' for s in blocks.values()), 'envelope_checked': envelope} PY /opt/venv/bin/python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 174, in validate_scene assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: detached decoration (6, 11, 19) -> (6, 11, 20)
#15 · 2026-09-24T21:57:36.180Z · exit=0 · 5s · python - <<'PY'
python - <<'PY' from generate import Build b=Build().build();s=b.scene print(b.plan.parameters) for v in b.plan.volumes: print(v) q=(6,11,19); print('cell',s.get(*q),'support',s.get(6,11,20)) print('near stairs',[p for p in s.stairs if abs(p[0]-q[0])<2 and abs(p[2]-q[2])<2]) print('windows',[w for w in s.windows if q in w['outside'] or (6,11,20) in w['cells']]) b.export(__import__('pathlib').Path('output.litematic')) PY xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews/initial

记录中的输出末尾

attic=False, parent='main', floor_step=6) cell air support stone_bricks near stairs [] windows [] 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 42 ms Set structure in renderer in 408 ms Rendering output at azimuth 0 (1/1)... Rendered image in 107 ms (azimuth: 3.6651914291880923, elevation: 0.3141592653589793) wrote /work/generator/workspace/previews/initial/front.png (512x512, azimuth 210, elevation 18) Loaded structure from /work/generator/workspace/output.litematic in 36 ms Set structure in renderer in 335 ms Rendering output at azimuth 0 (1/1)... Rendered image in 54 ms (azimuth: 0.6108652381980153, elevation: 0.3839724354387525) wrote /work/generator/workspace/previews/initial/rear.png (512x512, azimuth 35, elevation 22) Loaded structure from /work/generator/workspace/output.litematic in 32 ms Set structure in renderer in 305 ms Rendering output at azimuth 0 (1/1)... Rendered image in 46 ms (azimuth: 2.0943951023931953, elevation: 0.24434609527920614) wrote /work/generator/workspace/previews/initial/side.png (512x512, azimuth 120, elevation 14) Loaded structure from /work/generator/workspace/output.litematic in 39 ms Set structure in renderer in 350 ms Rendering output at azimuth 0 (1/1)... Rendered image in 51 ms (azimuth: 3.6651914291880923, elevation: 1.0821041362364843) wrote /work/generator/workspace/previews/initial/top.png (512x512, azimuth 210, elevation 62)
#16 · 2026-09-24T21:57:50.114Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace("if any(c in self.reserved for c in cells + inside):", "if any(c in self.reserved for c in cells + inside + outside):").replace("if any(self.get(*q) not in ('air',) for q in inside + outside):", "if any(any(v.interior(*q) for v in self.plan.volumes) for q in outside):\n return\n if any(self.get(*q) not in ('air',) for q in inside + outside):");open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 174, in validate_scene assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: detached decoration (15, 29, 23) -> (14, 29, 23)
#17 · 2026-09-24T21:58:07.742Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read();s=s.replace("for link in p.links:\n", "for v in p.volumes:\n if v.kind == 'stone':\n f = v.levels[-1]\n zz = (v.z0 + v.z1) // 2\n for x in range(v.x1 - 1, v.x1 + 3):\n for z in range(zz - 2, zz + 3):\n self.reserve((x, f + 1, z), 3)\n for link in p.links:\n",1);open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 193, in validate_scene assert not leaks, f'weather envelope leaks to {leaks}' ^^^^^^^^^ AssertionError: weather envelope leaks to [(9, 5, 5), (9, 5, 15), (9, 11, 5), (9, 11, 15), (9, 17, 5), (9, 17, 15), (9, 23, 10), (9, 5, 25), (9, 11, 25), (9, 17, 25), (9, 23, 25), (9, 29, 25), (22, 5, 10)]
#18 · 2026-09-24T21:58:31.996Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from generate import Build from lodge.geometry import name_of from collections import deque s=Build().build().scene porous={'air','ladder','vine','lantern','chain','iron_bars','campfire','fern','short_grass','flower_pot','lightning_rod'} def air(p): n=s.get(*p) return n in porous or n.endswith(('_stairs','_slab','_fence','_wall','_leaves','_bed')) or n.startswith('potted_') a=s.enclosed[0];q=deque([a]);prev={a:None};end=None while q: p=q.popleft();x,y,z=p if x<-9 or z<-10 or y>55 or z>40 or x>37 or y<0: end=p;break for dx,dy,dz in ((1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)): b=(x+dx,y+dy,z+dz) if b not in prev and air(b):prev[b]=p;q.append(b) path=[] while end is not None:path.append((end,s.get(*end)));end=prev[end] print(list(reversed(path))) PY

记录中的输出末尾

[((9, 5, 5), 'air'), ((9, 6, 5), 'air'), ((9, 7, 5), 'air'), ((9, 7, 4), 'air'), ((9, 7, 3), 'air'), ((9, 7, 2), 'air'), ((9, 7, 1), 'air'), ((9, 7, 0), 'air'), ((9, 7, -1), 'air'), ((9, 7, -2), 'air'), ((9, 7, -3), 'air'), ((9, 7, -4), 'air'), ((9, 7, -5), 'air'), ((9, 7, -6), 'air'), ((9, 7, -7), 'air'), ((9, 7, -8), 'air'), ((9, 7, -9), 'air'), ((9, 7, -10), 'air'), ((9, 7, -11), 'air')]
#19 · 2026-09-24T21:58:32.057Z · exit=0 · 0s · rg --files previews/initial | head -6
rg --files previews/initial | head -6

记录中的输出末尾

previews/initial/rear.png previews/initial/top.png previews/initial/front.png previews/initial/side.png
#20 · 2026-09-24T21:58:44.180Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace("self.doors.append((x, y, z))", "if self.get(x, y + 2, z) == 'air':\n self.p(x, y + 2, z, self.glass)\n self.doors.append((x, y, z))");open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 193, in validate_scene assert not leaks, f'weather envelope leaks to {leaks}' ^^^^^^^^^ AssertionError: weather envelope leaks to [(9, 5, 5), (9, 5, 15), (9, 11, 5), (9, 11, 15), (9, 17, 5), (9, 17, 15), (9, 23, 10), (9, 5, 25), (9, 11, 25), (9, 17, 25), (9, 23, 25), (9, 29, 25), (22, 5, 10)]
#21 · 2026-09-24T21:59:11.221Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from generate import Build from collections import deque s=Build().build().scene porous={'air','ladder','vine','lantern','chain','iron_bars','campfire','fern','short_grass','flower_pot','lightning_rod'} def air(p): n=s.get(*p);return n in porous or n.endswith(('_stairs','_slab','_fence','_wall','_leaves','_bed')) or n.startswith('potted_') a=s.enclosed[0];q=deque([a]);prev={a:None};end=None while q: p=q.popleft();x,y,z=p if x<-9 or z<-10 or y>55 or z>40 or x>37 or y<0:end=p;break for dx,dy,dz in ((1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)): b=(x+dx,y+dy,z+dz) if b not in prev and air(b):prev[b]=p;q.append(b) path=[] while end is not None:path.append((end,s.get(*end)));end=prev[end] print(list(reversed(path))) PY

记录中的输出末尾

[((9, 5, 5), 'air'), ((8, 5, 5), 'air'), ((7, 5, 5), 'air'), ((6, 5, 5), 'air'), ((6, 6, 5), 'air'), ((6, 6, 6), 'air'), ((6, 6, 7), 'air'), ((6, 6, 8), 'air'), ((6, 6, 9), 'air'), ((6, 6, 10), 'air'), ((6, 6, 11), 'air'), ((6, 7, 11), 'air'), ((6, 8, 11), 'air'), ((6, 9, 11), 'air'), ((6, 9, 12), 'air'), ((6, 9, 13), 'air'), ((6, 9, 14), 'air'), ((6, 10, 14), 'air'), ((6, 11, 14), 'air'), ((5, 11, 14), 'spruce_fence'), ((5, 12, 14), 'air'), ((5, 13, 14), 'air'), ((6, 13, 14), 'spruce_stairs'), ((6, 14, 14), 'air'), ((6, 15, 14), 'air'), ((6, 16, 14), 'air'), ((6, 17, 14), 'air'), ((5, 17, 14), 'spruce_fence'), ((5, 18, 14), 'air'), ((5, 19, 14), 'air'), ((6, 19, 14), 'spruce_stairs'), ((6, 20, 14), 'air'), ((6, 21, 14), 'air'), ((6, 22, 14), 'air'), ((6, 23, 14), 'air'), ((5, 23, 14), 'spruce_fence'), ((5, 24, 14), 'air'), ((4, 24, 14), 'potted_fern'), ((3, 24, 14), 'air'), ((2, 24, 14), 'potted_fern'), ((2, 24, 13), 'air'), ((2, 24, 12), 'potted_fern'), ((2, 24, 11), 'air'), ((2, 24, 10), 'air'), ((2, 24, 9), 'potted_fern'), ((2, 24, 8), 'air'), ((2, 24, 7), 'potted_fern'), ((2, 24, 6), 'air'), ((1, 24, 6), 'air'), ((0, 24, 6), 'air'), ((0, 24, 5), 'air'), ((0, 24, 4), 'air'), ((-1, 24, 4), 'deepslate_tile_stairs'), ((-1, 24, 3), 'deepslate_tile_stairs'), ((-2, 24, 3), 'air'), ((-3, 24, 3), 'air'), ((-4, 24, 3), 'air'), ((-5, 24, 3), 'air'), ((-6, 24, 3), 'air'), ((-7, 24, 3), 'air'), ((-8, 24, 3), 'air'), ((-9, 24, 3), 'air'), ((-10, 24, 3), 'air')]
#22 · 2026-09-24T21:59:28.788Z · exit=1 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read();s=s.replace(' def route_reservations(self):', " def roof_covered(self, volume, x, y, z):\n return any(v is not volume and v.contains(x, z) and v.base <= y < v.roof(x, z) for v in self.plan.volumes)\n\n def route_reservations(self):") a=s.index(' overhang = 1');b=s.index(' def opening(',a);s=s[:a]+s[a:b].replace('self.occupied_elsewhere(', 'self.roof_covered(')+s[b:];open(p,'w').write(s) PY python generate.py

记录中的输出末尾

Traceback (most recent call last): File "/work/generator/workspace/generate.py", line 85, in <module> main() File "/work/generator/workspace/generate.py", line 78, in main build = generate(args.seed, params, args.output) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 56, in generate build.validation = validate_scene(build.scene) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/lodge/validation.py", line 193, in validate_scene assert not leaks, f'weather envelope leaks to {leaks}' ^^^^^^^^^ AssertionError: weather envelope leaks to [(9, 5, 5), (9, 5, 15), (9, 11, 5), (9, 11, 15), (9, 17, 5), (9, 17, 15), (9, 23, 10), (9, 5, 25), (9, 11, 25), (9, 17, 25), (9, 23, 25), (9, 29, 25), (22, 5, 10)]
#23 · 2026-09-24T21:59:47.311Z · exit=0 · 0s · cat > /tmp/lodge_leak.py <<'PY'
cat > /tmp/lodge_leak.py <<'PY' import sys sys.path.insert(0,'/work/generator/workspace') from generate import Build from collections import deque s=Build(int(sys.argv[1]) if len(sys.argv)>1 else 20260924).build().scene porous={'air','ladder','vine','lantern','chain','iron_bars','campfire','fern','short_grass','flower_pot','lightning_rod'} def air(p): n=s.get(*p);return n in porous or n.endswith(('_stairs','_slab','_fence','_wall','_leaves','_bed')) or n.startswith('potted_') a=s.enclosed[0];q=deque([a]);prev={a:None};end=None while q: p=q.popleft();x,y,z=p if not any(v.contains(x,z,4) for v in s.plan.volumes) or y>=s.plan.size_xyz[1]-1 or y<0:end=p;break for dx,dy,dz in ((1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)): b=(x+dx,y+dy,z+dz) if b not in prev and air(b):prev[b]=p;q.append(b) path=[] while end is not None:path.append((end,s.get(*end)));end=prev[end] print(list(reversed(path))[-35:]) PY python /tmp/lodge_leak.py

记录中的输出末尾

[((6, 14, 14), 'air'), ((6, 15, 14), 'air'), ((6, 16, 14), 'air'), ((6, 17, 14), 'air'), ((5, 17, 14), 'spruce_fence'), ((5, 18, 14), 'air'), ((5, 19, 14), 'air'), ((6, 19, 14), 'spruce_stairs'), ((6, 20, 14), 'air'), ((6, 21, 14), 'air'), ((6, 22, 14), 'air'), ((6, 23, 14), 'air'), ((5, 23, 14), 'spruce_fence'), ((5, 24, 14), 'air'), ((5, 25, 14), 'air'), ((5, 26, 14), 'air'), ((5, 27, 14), 'air'), ((5, 28, 14), 'air'), ((5, 29, 14), 'air'), ((5, 29, 15), 'air'), ((5, 29, 16), 'air'), ((5, 29, 17), 'air'), ((5, 29, 18), 'air'), ((5, 29, 19), 'oak_fence'), ((4, 29, 19), 'oak_fence'), ((3, 29, 19), 'oak_fence'), ((3, 29, 20), 'oak_fence'), ((2, 29, 20), 'air'), ((1, 29, 20), 'air'), ((0, 29, 20), 'air'), ((-1, 29, 20), 'air'), ((-2, 29, 20), 'air'), ((-3, 29, 20), 'air'), ((-4, 29, 20), 'air'), ((-5, 29, 20), 'air')]
#24 · 2026-09-24T22:00:01.895Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace("if v.contains(x, z):\n continue\n self.p(x, top, z, self.floor)", "if v.contains(x, z) or any(other is not v and other.contains(x, z, 2) and top <= other.roof(x, z) + 2 for other in p.volumes):\n continue\n self.p(x, top, z, self.floor)");open(p,'w').write(s) PY python generate.py

记录中的输出末尾

{ "output": "output.litematic", "seed": 20260924, "bounds": [ 46, 48, 58 ], "validation": { "rooms": 13, "beds": 2, "doors": 2, "windows": 54, "walking_stairs": 51, "reachable_cells": 1577, "blocks": 16992, "envelope_checked": true } }
#25 · 2026-09-24T22:00:17.575Z · exit=0 · 7s · python - <<'PY'
python - <<'PY' from generate import generate from lodge.plan import resolve for seed in range(40): try: b=generate(seed) print(seed,'OK',b.plan.parameters['topology'],b.plan.parameters['tower'],b.validation['rooms']) except Exception as e: print(seed,type(e).__name__,str(e)[:200],resolve(seed).parameters) PY

记录中的输出末尾

4 OK staggered stone 8 35 OK staggered none 11 36 AssertionError weather envelope leaks to [(7, 8, 4), (7, 8, 12), (7, 14, 4), (7, 14, 12), (7, 20, 8), (-6, 8, 8), (20, 8, 6), (7, 8, 21), (7, 14, 21), (7, 20, 21), (7, 26, 21), (30, 8, 6)] {'topology': 'twin', 'width': 15, 'depth': 17, 'floors': 2, 'ridge': 'z', 'profile': 'chalet', 'wing_size': 13, 'wing_floors': 1, 'wing_ridge': 'parallel', 'placement': 'middle', 'tower': 'timber', 'tower_extra': 2, 'tower_width': 11, 'conservatory': True, 'porch': 'entry', 'balcony': False, 'dormers': 2, 'terrain': 'terrace', 'approach': 'landing', 'entrance': 'left', 'layout': 'cross', 'stair_side': 'right', 'palette': 'honey', 'trees': 4, 'wrap_side': None} 37 OK staggered stone 17 38 AssertionError overwritten window {'cells': [(16, 18, -5), (16, 19, -5), (16, 18, -4), (16, 19, -4)], 'inside': [(17, 18, -5), (17, 19, -5), (17, 18, -4), (17, 19, -4)], 'outside': [(15, 18, -5), (15, 19, -5), (15, {'topology': 'courtyard', 'width': 17, 'depth': 21, 'floors': 1, 'ridge': 'z', 'profile': 'steep', 'wing_size': 13, 'wing_floors': 1, 'wing_ridge': 'cross', 'placement': 'front', 'tower': 'timber', 'tower_extra': 1, 'tower_width': 11, 'conservatory': True, 'porch': 'full', 'balcony': False, 'dormers': 2, 'terrain': 'cliff', 'approach': 'landing', 'entrance': 'left', 'layout': 'suites', 'stair_side': 'right', 'palette': 'honey', 'trees': 5, 'wrap_side': None} 39 AssertionError weather envelope leaks to [(8, 5, 4), (8, 5, 13), (8, 11, 9), (21, 5, 14), (8, 5, 22), (8, 11, 22), (29, 5, 14)] {'topology': 'ell', 'width': 17, 'depth': 19, 'floors': 1, 'ridge': 'z', 'profile': 'chalet', 'wing_size': 11, 'wing_floors': 1, 'wing_ridge': 'cross', 'placement': 'rear', 'tower': 'stone', 'tower_extra': 1, 'tower_width': 9, 'conservatory': True, 'porch': 'wrap', 'balcony': False, 'dormers': 1, 'terrain': 'garden', 'approach': 'straight', 'entrance': 'left', 'layout': 'cross', 'stair_side': 'right', 'palette': 'honey', 'trees': 4, 'wrap_side': 'left'}
#26 · 2026-09-24T22:00:43.155Z · exit=0 · 0s · python /tmp/lodge_leak.py 1
python /tmp/lodge_leak.py 1

记录中的输出末尾

[((9, 8, 8), 'air'), ((8, 8, 8), 'air'), ((7, 8, 8), 'spruce_stairs'), ((6, 8, 8), 'spruce_stairs'), ((5, 8, 8), 'air'), ((4, 8, 8), 'air'), ((3, 8, 8), 'air'), ((2, 8, 8), 'air'), ((1, 8, 8), 'air'), ((1, 9, 8), 'air'), ((1, 10, 8), 'vine'), ((1, 10, 7), 'air'), ((1, 10, 6), 'air'), ((1, 10, 5), 'air'), ((0, 10, 5), 'air'), ((-1, 10, 5), 'air'), ((-1, 11, 5), 'air'), ((-1, 12, 5), 'air'), ((-1, 12, 4), 'air'), ((-1, 12, 3), 'ladder'), ((-1, 13, 3), 'ladder'), ((-1, 14, 3), 'ladder'), ((-1, 14, 2), 'air'), ((-1, 14, 1), 'air'), ((-1, 14, 0), 'air'), ((-1, 14, -1), 'air'), ((-1, 14, -2), 'deepslate_tile_slab'), ((-1, 14, -3), 'air'), ((-1, 14, -4), 'air'), ((-1, 14, -5), 'air'), ((-1, 14, -6), 'air'), ((-1, 14, -7), 'air')]
#27 · 2026-09-24T22:00:43.157Z · exit=0 · 0s · python /tmp/lodge_leak.py 18
python /tmp/lodge_leak.py 18

记录中的输出末尾

[((7, 14, 29), 'ladder'), ((7, 15, 29), 'ladder'), ((7, 16, 29), 'ladder'), ((7, 17, 29), 'ladder'), ((7, 18, 29), 'ladder'), ((7, 19, 29), 'ladder'), ((7, 20, 29), 'ladder'), ((7, 21, 29), 'ladder'), ((7, 22, 29), 'ladder'), ((7, 23, 29), 'ladder'), ((6, 23, 29), 'air'), ((5, 23, 29), 'air'), ((4, 23, 29), 'air'), ((3, 23, 29), 'air'), ((3, 24, 29), 'air'), ((3, 25, 29), 'air'), ((3, 26, 29), 'air'), ((3, 27, 29), 'air'), ((3, 27, 28), 'air'), ((3, 27, 27), 'air'), ((3, 27, 26), 'air'), ((3, 27, 25), 'air'), ((3, 27, 24), 'air'), ((3, 27, 23), 'air'), ((3, 27, 22), 'air'), ((3, 27, 21), 'air'), ((3, 27, 20), 'stone_brick_slab'), ((2, 27, 20), 'stone_brick_slab'), ((1, 27, 20), 'stone_brick_slab'), ((0, 27, 20), 'stone_brick_slab'), ((-1, 27, 20), 'air'), ((-2, 27, 20), 'air'), ((-3, 27, 20), 'air'), ((-4, 27, 20), 'air'), ((-5, 27, 20), 'air')]
#28 · 2026-09-24T22:00:43.172Z · exit=0 · 0s · python /tmp/lodge_leak.py 33
python /tmp/lodge_leak.py 33

记录中的输出末尾

[((3, 8, 10), 'air'), ((4, 8, 10), 'air'), ((5, 8, 10), 'air'), ((6, 8, 10), 'air'), ((7, 8, 10), 'air'), ((8, 8, 10), 'air'), ((8, 9, 10), 'air'), ((8, 10, 10), 'air'), ((8, 11, 10), 'air'), ((8, 12, 10), 'air'), ((8, 12, 9), 'air'), ((8, 12, 8), 'air'), ((8, 13, 8), 'air'), ((8, 14, 8), 'air'), ((7, 14, 8), 'air'), ((7, 14, 7), 'air'), ((7, 14, 6), 'air'), ((7, 14, 5), 'air'), ((7, 14, 4), 'air'), ((7, 14, 3), 'air'), ((7, 14, 2), 'air'), ((7, 14, 1), 'air'), ((6, 14, 1), 'air'), ((6, 14, 0), 'air'), ((6, 14, -1), 'deepslate_tile_slab'), ((6, 14, -2), 'deepslate_tile_slab'), ((6, 14, -3), 'air'), ((6, 14, -4), 'air'), ((6, 14, -5), 'air'), ((6, 14, -6), 'air')]
#29 · 2026-09-24T22:00:43.178Z · exit=0 · 0s · python /tmp/lodge_leak.py 39
python /tmp/lodge_leak.py 39

记录中的输出末尾

[((10, 6, 7), 'air'), ((10, 6, 8), 'air'), ((10, 6, 9), 'air'), ((10, 6, 10), 'air'), ((10, 7, 10), 'air'), ((10, 8, 10), 'air'), ((10, 9, 10), 'air'), ((10, 9, 11), 'air'), ((10, 9, 12), 'air'), ((10, 10, 12), 'air'), ((10, 11, 12), 'air'), ((9, 11, 12), 'air'), ((8, 11, 12), 'air'), ((7, 11, 12), 'air'), ((7, 12, 12), 'air'), ((6, 12, 12), 'potted_fern'), ((5, 12, 12), 'potted_fern'), ((5, 12, 11), 'air'), ((4, 12, 11), 'air'), ((3, 12, 11), 'potted_fern'), ((3, 12, 10), 'air'), ((3, 12, 9), 'air'), ((3, 12, 8), 'potted_fern'), ((3, 12, 7), 'air'), ((3, 12, 6), 'potted_fern'), ((2, 12, 6), 'air'), ((1, 12, 6), 'potted_fern'), ((0, 12, 6), 'air'), ((0, 11, 6), 'air'), ((-1, 11, 6), 'deepslate_tile_slab'), ((-2, 11, 6), 'deepslate_tile_slab'), ((-3, 11, 6), 'air'), ((-4, 11, 6), 'air'), ((-5, 11, 6), 'air'), ((-6, 11, 6), 'air')]
#30 · 2026-09-24T22:00:43.184Z · exit=0 · 0s · python /tmp/lodge_leak.py 36
python /tmp/lodge_leak.py 36

记录中的输出末尾

[((7, 20, 25), 'ladder'), ((7, 21, 25), 'ladder'), ((7, 22, 25), 'ladder'), ((7, 23, 25), 'ladder'), ((7, 24, 25), 'ladder'), ((7, 25, 25), 'ladder'), ((7, 26, 25), 'ladder'), ((6, 26, 25), 'air'), ((5, 26, 25), 'air'), ((4, 26, 25), 'air'), ((3, 26, 25), 'air'), ((3, 27, 25), 'air'), ((3, 28, 25), 'air'), ((3, 29, 25), 'air'), ((3, 30, 25), 'air'), ((3, 30, 24), 'air'), ((3, 30, 23), 'air'), ((3, 30, 22), 'air'), ((3, 30, 21), 'air'), ((3, 30, 20), 'air'), ((3, 30, 19), 'air'), ((3, 30, 18), 'air'), ((3, 30, 17), 'air'), ((3, 30, 16), 'stone_brick_slab'), ((2, 30, 16), 'stone_brick_slab'), ((1, 30, 16), 'stone_brick_slab'), ((0, 30, 16), 'stone_brick_slab'), ((-1, 30, 16), 'air'), ((-2, 30, 16), 'air'), ((-3, 30, 16), 'air'), ((-3, 30, 17), 'air'), ((-3, 30, 18), 'air'), ((-3, 30, 19), 'air'), ((-3, 30, 20), 'air'), ((-3, 30, 21), 'air')]
#31 · 2026-09-24T22:00:43.244Z · exit=0 · 0s · python /tmp/lodge_leak.py 3
python /tmp/lodge_leak.py 3

记录中的输出末尾

[((8, 17, 10), 'air'), ((9, 17, 10), 'air'), ((10, 17, 10), 'air'), ((11, 17, 10), 'air'), ((12, 17, 10), 'air'), ((12, 18, 10), 'air'), ((12, 19, 10), 'air'), ((12, 20, 10), 'air'), ((12, 21, 10), 'air'), ((12, 21, 9), 'air'), ((12, 21, 8), 'air'), ((12, 22, 8), 'air'), ((12, 23, 8), 'air'), ((11, 23, 8), 'air'), ((10, 23, 8), 'air'), ((9, 23, 8), 'air'), ((9, 24, 8), 'air'), ((9, 24, 7), 'air'), ((8, 24, 7), 'air'), ((7, 24, 7), 'air'), ((7, 24, 6), 'potted_fern'), ((6, 24, 6), 'air'), ((6, 24, 5), 'potted_fern'), ((6, 24, 4), 'air'), ((6, 24, 3), 'potted_fern'), ((6, 24, 2), 'air'), ((6, 24, 1), 'potted_fern'), ((6, 24, 0), 'air'), ((6, 23, 0), 'air'), ((6, 23, -1), 'deepslate_tile_slab'), ((6, 23, -2), 'deepslate_tile_slab'), ((6, 23, -3), 'air'), ((6, 23, -4), 'air'), ((6, 23, -5), 'air'), ((6, 23, -6), 'air')]
#32 · 2026-09-24T22:00:43.247Z · exit=0 · 0s · python /tmp/lodge_leak.py 17
python /tmp/lodge_leak.py 17

记录中的输出末尾

[((4, 17, 9), 'air'), ((3, 17, 9), 'air'), ((2, 17, 9), 'air'), ((1, 17, 9), 'air'), ((1, 18, 9), 'air'), ((1, 19, 9), 'air'), ((1, 19, 8), 'air'), ((1, 19, 7), 'air'), ((0, 19, 7), 'air'), ((-1, 19, 7), 'air'), ((-1, 20, 7), 'air'), ((-1, 21, 7), 'air'), ((-1, 21, 6), 'air'), ((-1, 21, 5), 'ladder'), ((-1, 22, 5), 'ladder'), ((-1, 23, 5), 'ladder'), ((-1, 23, 4), 'air'), ((-1, 23, 3), 'air'), ((-1, 23, 2), 'air'), ((-1, 23, 1), 'air'), ((-1, 23, 0), 'air'), ((-1, 23, -1), 'air'), ((-1, 23, -2), 'deepslate_tile_slab'), ((-1, 23, -3), 'air'), ((-1, 23, -4), 'air'), ((-1, 23, -5), 'air'), ((-1, 23, -6), 'air'), ((-1, 23, -7), 'air')]
#33 · 2026-09-24T22:01:39.303Z · exit=0 · 11s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read() s=s.replace("for x in range(ex - 1, ex + 2):\n for z in range(-3, 4):", "for x in range(ex - 3, ex + 4):\n for z in range(-3, 4):") s=s.replace("self.box((x - 1, h + 2, z - 1, x + 2, h + 2, z + 2), 'stone_brick_slab')", "self.box((x - 1, h + 2, z - 1, x + 2, h + 2, z + 2), 'stone_bricks')") s=s.replace(" def porches(self):", " def exterior_p(self, x, y, z, name, **props):\n if any(v.contains(x, z) and v.base < y <= v.roof(x, z) + 1 for v in self.plan.volumes):\n return\n if (x, y, z) in self.reserved or self.get(x, y, z) != 'air':\n return\n self.p(x, y, z, name, **props)\n\n def porches(self):") s=s.replace("if not any(v.interior(x, h, z) for v in p.volumes):\n self.p(x, h, z, self.rng.choice(ROOF))\n self.p(x, h + 1, z, 'deepslate_tile_slab')", "self.exterior_p(x, h, z, self.rng.choice(ROOF))\n self.exterior_p(x, h + 1, z, 'deepslate_tile_slab')") s=s.replace("if abs(x - ex) <= 1:\n continue", "if abs(x - ex) <= 1 or any(v.contains(x, z0) for v in p.volumes):\n continue") s=s.replace("self.box((x0, f + 4, z0, x1, f + 4, z0), self.beam, axis='x')", "for x in range(x0, x1 + 1):\n self.exterior_p(x, f + 4, z0, self.beam, axis='x')") s=s.replace("self.p(x, h, z, self.rng.choice(ROOF))\n self.p(x, h + 1, z, 'deepslate_tile_slab')", "self.exterior_p(x, h, z, self.rng.choice(ROOF))\n self.exterior_p(x, h + 1, z, 'deepslate_tile_slab')") open(p,'w').write(s) PY python -u - <<'PY' from generate import generate for seed in range(60): try: b=generate(seed) except Exception as e: print(seed,type(e).__name__,str(e)[:300],flush=True) print('corpus complete') PY

记录中的输出末尾

corpus complete
#34 · 2026-09-24T22:01:57.430Z · exit=0 · 3s · python generate.py
python generate.py RENDER_SIZE=700 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews/default

记录中的输出末尾

_stairs": 51, "reachable_cells": 1577, "blocks": 16992, "envelope_checked": true } } 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 48 ms Set structure in renderer in 426 ms Rendering output at azimuth 0 (1/1)... Rendered image in 62 ms (azimuth: 3.6651914291880923, elevation: 0.3141592653589793) wrote /work/generator/workspace/previews/default/front.png (700x700, azimuth 210, elevation 18) Loaded structure from /work/generator/workspace/output.litematic in 37 ms Set structure in renderer in 327 ms Rendering output at azimuth 0 (1/1)... Rendered image in 53 ms (azimuth: 0.6108652381980153, elevation: 0.3839724354387525) wrote /work/generator/workspace/previews/default/rear.png (700x700, azimuth 35, elevation 22) Loaded structure from /work/generator/workspace/output.litematic in 32 ms Set structure in renderer in 302 ms Rendering output at azimuth 0 (1/1)... Rendered image in 43 ms (azimuth: 2.0943951023931953, elevation: 0.24434609527920614) wrote /work/generator/workspace/previews/default/side.png (700x700, azimuth 120, elevation 14) Loaded structure from /work/generator/workspace/output.litematic in 37 ms Set structure in renderer in 311 ms Rendering output at azimuth 0 (1/1)... Rendered image in 50 ms (azimuth: 3.6651914291880923, elevation: 1.0821041362364843) wrote /work/generator/workspace/previews/default/top.png (700x700, azimuth 210, elevation 62)
#35 · 2026-09-24T22:03:40.215Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from lodge.plan import resolve for n in (0,2,3,5,8,13,21,28,31,37,42,20260924): p=resolve(n).parameters print(n,{k:p[k] for k in ('topology','floors','ridge','profile','tower','conservatory','terrain','layout','porch')}) PY

记录中的输出末尾

, 'ridge': 'x', 'profile': 'steep', 'tower': 'none', 'conservatory': False, 'terrain': 'cliff', 'layout': 'open', 'porch': 'entry'} 2 {'topology': 'single', 'floors': 2, 'ridge': 'z', 'profile': 'bell', 'tower': 'none', 'conservatory': True, 'terrain': 'cliff', 'layout': 'cross', 'porch': 'wrap'} 3 {'topology': 'ell', 'floors': 1, 'ridge': 'x', 'profile': 'bell', 'tower': 'stone', 'conservatory': True, 'terrain': 'cliff', 'layout': 'suites', 'porch': 'full'} 5 {'topology': 'staggered', 'floors': 2, 'ridge': 'z', 'profile': 'steep', 'tower': 'stone', 'conservatory': True, 'terrain': 'garden', 'layout': 'open', 'porch': 'wrap'} 8 {'topology': 'ell', 'floors': 1, 'ridge': 'z', 'profile': 'bell', 'tower': 'timber', 'conservatory': False, 'terrain': 'terrace', 'layout': 'open', 'porch': 'full'} 13 {'topology': 'twin', 'floors': 3, 'ridge': 'z', 'profile': 'bell', 'tower': 'timber', 'conservatory': False, 'terrain': 'terrace', 'layout': 'open', 'porch': 'entry'} 21 {'topology': 'ell', 'floors': 2, 'ridge': 'x', 'profile': 'steep', 'tower': 'timber', 'conservatory': False, 'terrain': 'garden', 'layout': 'suites', 'porch': 'full'} 28 {'topology': 'single', 'floors': 3, 'ridge': 'z', 'profile': 'chalet', 'tower': 'none', 'conservatory': False, 'terrain': 'garden', 'layout': 'open', 'porch': 'entry'} 31 {'topology': 'single', 'floors': 2, 'ridge': 'z', 'profile': 'bell', 'tower': 'none', 'conservatory': False, 'terrain': 'garden', 'layout': 'cross', 'porch': 'wrap'} 37 {'topology': 'staggered', 'floors': 3, 'ridge': 'z', 'profile': 'bell', 'tower': 'stone', 'conservatory': True, 'terrain': 'garden', 'layout': 'suites', 'porch': 'full'} 42 {'topology': 'single', 'floors': 2, 'ridge': 'z', 'profile': 'chalet', 'tower': 'stone', 'conservatory': False, 'terrain': 'cliff', 'layout': 'open', 'porch': 'entry'} 20260924 {'topology': 'single', 'floors': 3, 'ridge': 'z', 'profile': 'bell', 'tower': 'stone', 'conservatory': True, 'terrain': 'garden', 'layout': 'cross', 'porch': 'full'}
#36 · 2026-09-24T22:03:52.185Z · exit=0 · 11s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read();a=s.index(' def openings(self):');s=s[:a]+''' def gable_details(self): """Frame vaulted attic gables with tall paired lights and collar beams.""" v = self.plan.volumes[0] f = v.levels[-1] for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')): dx, dz = DIRECTIONS[face] peak = v.roof(*v.xyz(v.uc, 0, vv)[::2]) for y in (f + 6, f + 12): for u in range(v.u0 + 2, v.u1 - 1): x, _, z = v.xyz(u, 0, vv) if y < v.roof(x, z) - 2 and not self.occupied_elsewhere(v, x, y, z): self.p(x, y, z, self.beam, axis='x' if v.axis == 'z' else 'z') for u in (v.uc - 1, v.uc + 1): x, _, z = v.xyz(u, 0, vv) top = min(f + 9, v.roof(x, z) - 3) cells = [(x, y, z) for y in range(f + 3, top + 1)] inside = [(x - dx, y, z - dz) for _, y, _ in cells] outside = [(x + dx, y, z + dz) for _, y, _ in cells] if cells and all(q not in self.reserved and self.get(*q) == 'air' for q in inside + outside) and not any(any(other.interior(*q) for other in self.plan.volumes) for q in outside): for q in cells: self.p(*q, self.glass) self.windows.append({'cells': cells, 'inside': inside, 'outside': outside}) self.reserved.update(cells + inside + outside) x, _, z = v.xyz(v.uc, 0, vv) for y in range(f + 2, peak): if not self.occupied_elsewhere(v, x, y, z): self.p(x, y, z, self.timber, axis='y') # Corbels and small low lamps make the timber frame read in relief. for v in self.plan.volumes: if v.kind in ('glass', 'dormer'): continue for f in v.levels[1:]: for x in range(v.x0, v.x1 + 1, 4): for z, face in ((v.z0 - 1, 'south'), (v.z1 + 1, 'north')): dx, dz = DIRECTIONS[face] if self.get(x + dx, f - 1, z + dz) != 'air': self.exterior_p(x, f - 1, z, 'spruce_stairs', facing=face, half='top') for x in (v.x0, v.x1): z = v.z0 - 1 y = v.base + 3 if all(self.get(x, yy, z) == 'air' and (x, yy, z) not in self.reserved for yy in (y, y + 1)) and not any(other.contains(x, z) for other in self.plan.volumes): self.p(x, y + 1, z, self.beam, axis='z') self.lamp(x, y, z, True) ''' + s[a:];s=s.replace(' self.openings()\n', ' self.gable_details()\n self.openings()\n');open(p,'w').write(s) PY python -u - <<'PY' from generate import generate for seed in range(60): try: b=generate(seed) except Exception as e: print(seed,type(e).__name__,str(e)[:220],flush=True) print('corpus complete') PY

记录中的输出末尾

corpus complete
#37 · 2026-09-24T22:04:45.183Z · exit=0 · 20s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read();s=s.replace("vc = (v.v0 + v.v1) // 2\n segments", "vc = (v.v0 + v.v1) // 2 + (2 if layout == 'suites' else 0)\n segments") s=s.replace(" for index, (va, vb) in enumerate(segments):", " if layout == 'suites' and level > 0:\n for u in (v.uc - 1, v.uc + 1):\n for yy in range(f + 1, f + 6):\n self.p(*v.xyz(u, yy, vc), self.timber, axis='y')\n self.door(*v.xyz(v.uc, f + 1, vc), 'south' if v.axis == 'z' else 'east')\n for index, (va, vb) in enumerate(segments):") s=s.replace("for x in range(v.x0 + 1, v.x1):\n for z in range(v.z0 + 1, v.z1):", "for x in range(x0, x1 + 1):\n for z in range(z0, z1 + 1):") open(p,'w').write(s) PY python -u - <<'PY' from generate import generate for seed in list(range(96)) + [-(2**63),2**63-1,20260924,20260923]: try: b=generate(seed) except Exception as e: print(seed,type(e).__name__,str(e)[:240],flush=True) print('100-seed corpus complete') PY

记录中的输出末尾

88 AssertionError blocked window {'cells': [(0, 6, -5), (0, 7, -5), (0, 6, -4), (0, 7, -4)], 'inside': [(-1, 6, -5), (-1, 7, -5), (-1, 6, -4), (-1, 7, -4)], 'outside': [(1, 6, -5), (1, 7, -5), (1, 6, -4), (1, 7, -4)]} 100-seed corpus complete
#38 · 2026-09-24T22:05:16.235Z · exit=0 · 2s · python - <<'PY'
python - <<'PY' from generate import generate from lodge.validation import reachable,walk_passable,solid_support for seed in range(12): b=generate(seed);s=b.scene;reached=reachable(s);bad=[] for v in b.plan.volumes: for f in v.levels: for x in range(v.x0+1,v.x1): for z in range(v.z0+1,v.z1): if solid_support(s.get(x,f,z)) and all(walk_passable(s.get(x,f+dy,z)) for dy in (1,2)) and (x,f+1,z) not in reached:bad.append((v.name,x,f+1,z)) print(seed,'unreachable floor cells',len(bad),bad[:10]) PY

记录中的输出末尾

0 unreachable floor cells 0 [] 1 unreachable floor cells 0 [] 2 unreachable floor cells 12 [('main', 2, 29, 13), ('main', 3, 29, 13), ('main', 3, 29, 14), ('main', 3, 29, 15), ('main', 10, 29, 1), ('main', 11, 29, 1), ('dormer_0', 0, 29, 4), ('dormer_0', 0, 29, 5), ('dormer_0', 0, 29, 6), ('dormer_1', 14, 29, 10)] 3 unreachable floor cells 11 [('main', 1, 23, 14), ('main', 1, 23, 15), ('main', 1, 23, 17), ('main', 2, 23, 17), ('main', 3, 23, 17), ('main', 3, 23, 18), ('main', 17, 23, 14), ('main', 17, 23, 15), ('dormer_0', 4, 23, 0), ('dormer_0', 5, 23, 0)] 4 unreachable floor cells 0 [] 5 unreachable floor cells 0 [] 6 unreachable floor cells 12 [('main', 2, 29, 15), ('main', 3, 29, 15), ('main', 3, 29, 16), ('main', 3, 29, 17), ('main', 10, 29, 1), ('main', 11, 29, 1), ('dormer_0', 0, 29, 4), ('dormer_0', 0, 29, 5), ('dormer_0', 0, 29, 6), ('dormer_1', 14, 29, 12)] 7 unreachable floor cells 4 [('main', 10, 23, 1), ('main', 10, 23, 17), ('main', 11, 23, 1), ('main', 11, 23, 17)] 8 unreachable floor cells 7 [('main', 2, 14, 15), ('main', 3, 14, 15), ('main', 3, 14, 16), ('main', 3, 14, 17), ('dormer_0', 0, 14, 4), ('dormer_0', 0, 14, 5), ('dormer_0', 0, 14, 6)] 9 unreachable floor cells 6 [('main', 2, 29, 15), ('main', 3, 29, 15), ('main', 3, 29, 16), ('main', 3, 29, 17), ('main', 14, 29, 1), ('main', 15, 29, 1)] 10 unreachable floor cells 12 [('main', 2, 20, 15), ('main', 3, 20, 15), ('main', 3, 20, 16), ('main', 3, 20, 17), ('main', 10, 20, 1), ('main', 11, 20, 1), ('dormer_0', 0, 20, 4), ('dormer_0', 0, 20, 5), ('dormer_0', 0, 20, 6), ('dormer_1', 14, 20, 12)] 11 unreachable floor cells 14 [('main', 2, 20, 15), ('main', 3, 20, 15), ('main', 3, 20, 16), ('main', 3, 20, 17), ('main', 14, 20, 1), ('main', 14, 20, 17), ('main', 15, 20, 1), ('main', 15, 20, 17), ('dormer_0', 0, 20, 4), ('dormer_0', 0, 20, 5)]
#39 · 2026-09-24T22:05:31.477Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from generate import Build from lodge.validation import reachable,walk_passable s=Build(2).build().scene;v=s.plan.volumes[0];f=v.levels[-1];reach=reachable(s) print(s.plan.parameters,v) for z in range(v.z0-1,v.z1+2): line='' for x in range(v.x0-2,v.x1+3): n=s.get(x,f+1,z);h=s.get(x,f+2,z) c='.' if (x,f+1,z) in reach else '?' if n=='air' and h=='air' else 'L' if n=='ladder' else 'b' if n.endswith('_bed') else 'F' if n in ('barrel','bookshelf','chest','loom') else 'S' if 'stairs' in n else '#' if n!='air' else '^' line+=c print(z,line) print('seed88',Build(88).plan.parameters) s=Build(88).build().scene for q in ((1,6,-5),(1,7,-5),(1,6,-4),(1,7,-4)):print(q,s.get(*q)) PY

记录中的输出末尾

{'topology': 'single', 'width': 15, 'depth': 17, 'floors': 2, 'ridge': 'z', 'profile': 'bell', 'wing_size': 13, 'wing_floors': 2, 'wing_ridge': 'cross', 'placement': 'rear', 'tower': 'none', 'tower_extra': 1, 'tower_width': 9, 'conservatory': True, 'porch': 'wrap', 'balcony': True, 'dormers': 2, 'terrain': 'cliff', 'approach': 'landing', 'entrance': 'right', 'layout': 'cross', 'stair_side': 'right', 'palette': 'honey', 'trees': 3, 'wrap_side': 'left'} Volume(name='main', x0=0, z0=0, x1=14, z1=16, base=16, floors=2, axis='z', profile='bell', kind='house', attic=True, parent=None, floor_step=6) -1 S##^???????^##^^##S 0 S#################S 1 S##^FF#F...F??F^##S 2 S##^F#F#...FFF#^##S 3 S##F#FFF...###F^##S 4 S#?bFFFF...FFFF^##S 5 ##?bFFFF...FFFF^##S 6 ##?FFF#F...FFFF^##S 7 S##FF#F#......#^##S 8 S##^...........^##S 9 S##^#FFF.....#FF##S 10 S##^FFFF...??#FF?#S 11 S##^FFFF...??#F#?## 12 S##^FF#F...??#FF?## 13 S##^??F#.....##F##S 14 S####?FF......F^##S 15 S####?FF......F^##S 16 S#################S 17 S##^???????????^##S seed88 {'topology': 'courtyard', 'width': 15, 'depth': 19, 'floors': 1, 'ridge': 'z', 'profile': 'bell', 'wing_size': 13, 'wing_floors': 1, 'wing_ridge': 'cross', 'placement': 'rear', 'tower': 'stone', 'tower_extra': 1, 'tower_width': 9, 'conservatory': False, 'porch': 'entry', 'balcony': False, 'dormers': 2, 'terrain': 'garden', 'approach': 'landing', 'entrance': 'left', 'layout': 'cross', 'stair_side': 'right', 'palette': 'honey', 'trees': 4, 'wrap_side': None} (1, 6, -5) air (1, 7, -5) lantern (1, 6, -4) stripped_spruce_log (1, 7, -4) stripped_spruce_log
#40 · 2026-09-24T22:07:11.183Z · exit=0 · 20s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read() s=s.replace("if v.kind == 'dormer':\n continue\n for floor", "if v.kind == 'dormer':\n for vv in range(v.v0 + 1, v.v1):\n self.reserve(v.xyz(v.uc, v.base + 1, vv), 3)\n continue\n for floor",1) s=s.replace(" for v in p.volumes:\n if v.kind == 'stone':", " px0, pz0, px1, _ = p.porch\n post_xs = {px0, px1}\n if px1 - px0 > 9:\n post_xs.update((p.entrance[0] - 3, p.entrance[0] + 3))\n for x in post_xs:\n for z in (pz0 - 1, pz0):\n for y in range(p.volumes[0].base + 1, p.volumes[0].base + 5):\n self.reserved.add((x, y, z))\n for v in p.volumes:\n if v.kind == 'stone':",1) s=s.replace("if any(q in self.reserved or self.get(*q) != 'air' or self.get(q[0], y - 1, q[2]) == 'air' for q in pts):", "if any(q in self.reserved or self.get(*q) != 'air' or not self.get(q[0], y - 1, q[2]) or self.get(q[0], y - 1, q[2]) == 'air' or self.get(q[0], y + 1, q[2]) != 'air' for q in pts):") s=s.replace(" if not spots:\n raise AssertionError", " # Bound furniture density and retain walkable gaps, including low attics.\n spaced = []\n for q in spots:\n if all(abs(q[0] - r[0]) + abs(q[2] - r[2]) >= 3 for r in spaced):\n spaced.append(q)\n spots = spaced[:max(4, min(12, ((ub - ua + 1) * (vb - va + 1)) // 16))]\n if not spots:\n raise AssertionError") s=s.replace(" self.landscape()", " self.dormer_nooks()\n self.landscape()") a=s.index(' def chimney(self):');s=s[:a]+''' def dormer_nooks(self): """Furnish and verify the accessible reading bays extending the attic.""" main = self.plan.volumes[0] for v in self.plan.volumes: if v.kind != 'dormer': continue exterior_v = v.v0 + 1 if v.v0 < main.uc else v.v1 - 1 target = v.xyz(v.uc, v.base + 1, exterior_v) room = self.room(v, v.base, 'dormer reading nook', (v.x0 + 1, v.base + 1, v.z0 + 1, v.x1 - 1, v.base + 4, v.z1 - 1), target) # The parent attic remains open behind the bay. Its own small lamp is near the floor. for vv in range(v.v0 + 1, v.v1): for u in (v.uc - 1, v.uc + 1): x, y, z = v.xyz(u, v.base + 1, vv) if all(self.get(x, y + dy, z) == 'air' and (x, y + dy, z) not in self.reserved for dy in (0, 1)): self.p(x, y, z, 'bookshelf') self.lamp(x, y + 1, z) room['furniture'].append((x, y, z)) room['lights'].append((x, y + 1, z)) break if room['lights']: break assert room['lights'], f'no task-light position for {v.name}' self.enclosed.append(target) ''' + s[a:] open(p,'w').write(s) p='lodge/plan.py';s=open(p).read().replace("first_side = rng.choice(('left', 'right'))", "first_side = rng.choice(('left', 'right'))\n if p['porch'] == 'wrap' and p['conservatory'] and count == 1:\n first_side = 'right'");open(p,'w').write(s) PY python -u - <<'PY' from generate import generate for seed in list(range(100)) + [-(2**63),2**63-1,20260924,20260923]: try:b=generate(seed) except Exception as e:print(seed,type(e).__name__,str(e)[:240],flush=True) print('104-seed corpus complete') PY

记录中的输出末尾

104-seed corpus complete
#41 · 2026-09-24T22:07:18.732Z · exit=0 · 2s · python - <<'PY'
python - <<'PY' from generate import generate from lodge.validation import reachable,walk_passable,solid_support for seed in range(12): b=generate(seed);s=b.scene;reached=reachable(s);bad=[] for v in b.plan.volumes: for f in v.levels: for x in range(v.x0+1,v.x1): for z in range(v.z0+1,v.z1): if solid_support(s.get(x,f,z)) and all(walk_passable(s.get(x,f+dy,z)) for dy in (1,2)) and (x,f+1,z) not in reached:bad.append((v.name,x,f+1,z)) print(seed,len(bad),bad[:7]) PY

记录中的输出末尾

0 0 [] 1 0 [] 2 0 [] 3 0 [] 4 0 [] 5 0 [] 6 0 [] 7 2 [('main', 2, 23, 11), ('main', 2, 23, 12)] 8 0 [] 9 0 [] 10 0 [] 11 0 []
#42 · 2026-09-24T22:07:48.772Z · exit=0 · 3s · python generate.py
python generate.py RENDER_SIZE=640 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews/default

记录中的输出末尾

_stairs": 51, "reachable_cells": 1803, "blocks": 16694, "envelope_checked": true } } 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 40 ms Set structure in renderer in 401 ms Rendering output at azimuth 0 (1/1)... Rendered image in 54 ms (azimuth: 3.6651914291880923, elevation: 0.3141592653589793) wrote /work/generator/workspace/previews/default/front.png (640x640, azimuth 210, elevation 18) Loaded structure from /work/generator/workspace/output.litematic in 38 ms Set structure in renderer in 297 ms Rendering output at azimuth 0 (1/1)... Rendered image in 49 ms (azimuth: 0.6108652381980153, elevation: 0.3839724354387525) wrote /work/generator/workspace/previews/default/rear.png (640x640, azimuth 35, elevation 22) Loaded structure from /work/generator/workspace/output.litematic in 36 ms Set structure in renderer in 288 ms Rendering output at azimuth 0 (1/1)... Rendered image in 44 ms (azimuth: 2.0943951023931953, elevation: 0.24434609527920614) wrote /work/generator/workspace/previews/default/side.png (640x640, azimuth 120, elevation 14) Loaded structure from /work/generator/workspace/output.litematic in 34 ms Set structure in renderer in 316 ms Rendering output at azimuth 0 (1/1)... Rendered image in 52 ms (azimuth: 3.6651914291880923, elevation: 1.0821041362364843) wrote /work/generator/workspace/previews/default/top.png (640x640, azimuth 210, elevation 62)
#43 · 2026-09-24T22:07:57.110Z · exit=0 · 12s · python - <<'PY'
python - <<'PY' from generate import Build from lodge.validation import reachable,walk_passable,solid_support for seed in range(100): b=Build(seed).build();s=b.scene;r=reachable(s);bad=set() for v in b.plan.volumes: for f in v.levels: for x in range(v.x0+1,v.x1): for z in range(v.z0+1,v.z1): if solid_support(s.get(x,f,z)) and all(walk_passable(s.get(x,f+dy,z)) for dy in (1,2)) and (x,f+1,z) not in r:bad.add((x,f+1,z)) if bad: print(seed,len(bad),sorted(bad)[:12]) PY

记录中的输出末尾

7 2 [(2, 23, 11), (2, 23, 12)] 32 1 [(2, 11, 11)] 64 3 [(1, 35, 6), (1, 35, 7), (2, 35, 6)] 73 3 [(12, 17, 13), (12, 17, 14), (12, 17, 15)] 85 3 [(1, 11, 6), (1, 11, 7), (2, 11, 6)]
#44 · 2026-09-24T22:08:03.502Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from generate import Build from lodge.validation import reachable s=Build(7).build().scene;v=s.plan.volumes[0];f=v.levels[-1];reach=reachable(s);print(v) for z in range(v.z0,v.z1+1): print(z,''.join('.' if (x,f+1,z) in reach else ' ' if s.get(x,f+1,z)=='air' and s.get(x,f+2,z)=='air' else s.get(x,f+1,z)[0] for x in range(v.x0,v.x1+1))) PY

记录中的输出末尾

Volume(name='main', x0=0, z0=0, x1=14, z1=18, base=4, floors=3, axis='z', profile='chalet', kind='house', attic=True, parent=None, floor_step=6) 0 dsssssssssssssd 1 dab......b...ad 2 da..c.....o..ad 3 da........s..ad 4 dar......l...ad 5 dar.b........ad 6 da...........ad 7 dac......c...ad 8 da..c........ac 9 da...........ad 10 dab......b...ad 11 ca s.........ad 12 da s .......ac 13 dals .......ad 14 da.s .......ad 15 da...........ad 16 css..........ad 17 dss..........ad 18 dsssssssssssssd
#45 · 2026-09-24T22:08:15.079Z · exit=0 · 12s · python - <<'PY'
python - <<'PY' from generate import Build from lodge.validation import reachable,walk_passable,solid_support for seed in range(100): b=Build(seed).build();s=b.scene;r=reachable(s);bad=set() for v in b.plan.volumes: for f in v.levels: for x in range(v.x0+1,v.x1): for z in range(v.z0+1,v.z1): if solid_support(s.get(x,f,z)) and all(walk_passable(s.get(x,f+dy,z)) for dy in (1,2)) and (x,f+1,z) not in r:bad.add((x,f+1,z)) if bad: print(seed,len(bad),sorted(bad)[:12]) PY

记录中的输出末尾

7 2 [(2, 23, 11), (2, 23, 12)] 32 1 [(2, 11, 11)] 64 3 [(1, 35, 6), (1, 35, 7), (2, 35, 6)] 73 3 [(12, 17, 13), (12, 17, 14), (12, 17, 15)] 85 3 [(1, 11, 6), (1, 11, 7), (2, 11, 6)]
#46 · 2026-09-24T22:08:18.079Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace("us = (v.uc + sign * 2, v.uc + sign * 3)\n for floor", "us = (v.uc + sign * 2, v.uc + sign * 3)\n for floor in v.levels:\n for vv in range(v.v1 - 9, v.v1):\n self.reserve(v.xyz(v.uc + sign * 5, floor + 1, vv))\n for floor");open(p,'w').write(s) PY

记录中的输出末尾

#47 · 2026-09-24T22:08:30.307Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from lodge.plan import resolve print(resolve(20260923).parameters) PY

记录中的输出末尾

{'topology': 'twin', 'width': 15, 'depth': 19, 'floors': 2, 'ridge': 'z', 'profile': 'steep', 'wing_size': 11, 'wing_floors': 1, 'wing_ridge': 'parallel', 'placement': 'rear', 'tower': 'none', 'tower_extra': 1, 'tower_width': 9, 'conservatory': True, 'porch': 'entry', 'balcony': False, 'dormers': 0, 'terrain': 'terrace', 'approach': 'straight', 'entrance': 'center', 'layout': 'suites', 'stair_side': 'right', 'palette': 'ivory', 'trees': 3, 'wrap_side': None}
#48 · 2026-09-24T22:08:31.174Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from generate import Build from lodge.validation import reachable,walk_passable,solid_support for seed in (7,32,64,73,85): b=Build(seed).build();s=b.scene;r=reachable(s);bad=set() for v in b.plan.volumes: for f in v.levels: for x in range(v.x0+1,v.x1): for z in range(v.z0+1,v.z1): if solid_support(s.get(x,f,z)) and all(walk_passable(s.get(x,f+dy,z)) for dy in (1,2)) and (x,f+1,z) not in r:bad.add((x,f+1,z)) print(seed,b.plan.parameters,len(bad),sorted(bad)) PY

记录中的输出末尾

alette': 'ochre', 'trees': 3, 'wrap_side': None} 0 [] 32 {'topology': 'single', 'width': 15, 'depth': 17, 'floors': 2, 'ridge': 'z', 'profile': 'steep', 'wing_size': 9, 'wing_floors': 1, 'wing_ridge': 'parallel', 'placement': 'middle', 'tower': 'timber', 'tower_extra': 2, 'tower_width': 9, 'conservatory': True, 'porch': 'full', 'balcony': False, 'dormers': 2, 'terrain': 'garden', 'approach': 'straight', 'entrance': 'left', 'layout': 'suites', 'stair_side': 'left', 'palette': 'ivory', 'trees': 3, 'wrap_side': None} 0 [] 64 {'topology': 'courtyard', 'width': 15, 'depth': 21, 'floors': 3, 'ridge': 'x', 'profile': 'bell', 'wing_size': 13, 'wing_floors': 1, 'wing_ridge': 'parallel', 'placement': 'middle', 'tower': 'timber', 'tower_extra': 1, 'tower_width': 9, 'conservatory': False, 'porch': 'full', 'balcony': True, 'dormers': 0, 'terrain': 'cliff', 'approach': 'straight', 'entrance': 'center', 'layout': 'cross', 'stair_side': 'left', 'palette': 'honey', 'trees': 3, 'wrap_side': None} 3 [(1, 35, 6), (1, 35, 7), (2, 35, 6)] 73 {'topology': 'twin', 'width': 15, 'depth': 21, 'floors': 2, 'ridge': 'z', 'profile': 'steep', 'wing_size': 11, 'wing_floors': 1, 'wing_ridge': 'cross', 'placement': 'middle', 'tower': 'timber', 'tower_extra': 1, 'tower_width': 11, 'conservatory': False, 'porch': 'entry', 'balcony': True, 'dormers': 1, 'terrain': 'garden', 'approach': 'landing', 'entrance': 'left', 'layout': 'suites', 'stair_side': 'right', 'palette': 'ivory', 'trees': 5, 'wrap_side': None} 0 [] 85 {'topology': 'ell', 'width': 19, 'depth': 21, 'floors': 1, 'ridge': 'x', 'profile': 'chalet', 'wing_size': 9, 'wing_floors': 1, 'wing_ridge': 'cross', 'placement': 'rear', 'tower': 'none', 'tower_extra': 2, 'tower_width': 9, 'conservatory': True, 'porch': 'wrap', 'balcony': False, 'dormers': 0, 'terrain': 'garden', 'approach': 'straight', 'entrance': 'left', 'layout': 'cross', 'stair_side': 'right', 'palette': 'ivory', 'trees': 5, 'wrap_side': 'left'} 3 [(1, 11, 6), (1, 11, 7), (2, 11, 6)]
#49 · 2026-09-24T22:09:29.814Z · exit=0 · 22s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace(" sign = -1 if p.parameters['stair_side'] == 'left' else 1", " for floor in v.levels:\n for u in range(v.u0 + 2, v.u1 - 1):\n for vv in (v.v0 + 1, v.v0 + 2, v.v1 - 2, v.v1 - 1):\n self.reserve(v.xyz(u, floor + 1, vv))\n sign = -1 if p.parameters['stair_side'] == 'left' else 1",1);open(p,'w').write(s) PY python -u - <<'PY' from generate import generate from lodge.validation import reachable,walk_passable,solid_support for seed in range(100): try: b=generate(seed);s=b.scene;r=reachable(s);bad=set() for v in b.plan.volumes: for f in v.levels: for x in range(v.x0+1,v.x1): for z in range(v.z0+1,v.z1): if solid_support(s.get(x,f,z)) and all(walk_passable(s.get(x,f+dy,z)) for dy in (1,2)) and (x,f+1,z) not in r:bad.add((x,f+1,z)) if bad:print(seed,'isolated',len(bad),sorted(bad)[:8]) except Exception as e:print(seed,type(e).__name__,str(e)[:220]) print('done') PY

记录中的输出末尾

done
#50 · 2026-09-24T22:09:45.605Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace(" if not spots:\n for x in range(x0", " if len(spots) < 6:\n for x in range(x0").replace("if i % 2 == 0 and (x, y + 1, z) not in self.reserved:", "if i % 2 == 0 and name not in ('composter', 'chest') and (x, y + 1, z) not in self.reserved:");open(p,'w').write(s) PY

记录中的输出末尾

#51 · 2026-09-25T01:17:23.734Z · exit=0 · 0s · pwd && rg --files -g '!references/**' -g '!previews/**' -g '!*.png' -g '!*.litematic' -g '!__pycache__/**' -g
pwd && rg --files -g '!references/**' -g '!previews/**' -g '!*.png' -g '!*.litematic' -g '!__pycache__/**' -g '!lodge/__pycache__/**' -g '!output.json' . && tail -45 lodge/geometry.py && cat lodge/validation.py

记录中的输出末尾

', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet'): assert solid_support(get(x, y - 1, z)), f'unsupported plant {(x,y,z)}' if n == 'vine': assert any(props.get(face) == 'true' and solid_support(get(x + dx, y, z + dz)) for face, (dx, dz) in DIRECTIONS.items()), f'unattached vine {(x,y,z)}' for q, support in scene.attachments: assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' for window in scene.windows: assert all('glass' in get(*q) for q in window['cells']), f'overwritten window {window}' assert all(get(*q) == 'air' for q in window['inside'] + window['outside']), f'blocked window {window}' for x, y, z, facing in scene.stairs: n, props = parse(blocks.get((x, y, z))) assert n.endswith('_stairs') and props['facing'] == facing, f'overwritten tread {(x,y,z)}' assert solid_support(get(x, y - 1, z)), f'unsupported tread {(x,y,z)}' assert all(walk_passable(get(x, y + dy, z)) for dy in (1, 2, 3)), f'stair headroom {(x,y,z)}' visited = reachable(scene) for room in scene.rooms: assert room['target'] in visited, f'unreachable {room["volume"]}/{room["purpose"]}: {room["target"]}' assert room['furniture'], f'unfurnished room {room}' assert room['lights'], f'unlit room {room}' assert all(get(*q) == 'lantern' for q in room['lights']), f'overwritten room lights {room}' assert all(abs(q[1] - room['floor']) <= 3 for q in room['lights']), f'light too high {room}' assert beds >= 1, 'no sleeping accommodation' if envelope: leaks = envelope_leaks(scene) assert not leaks, f'weather envelope leaks to {leaks}' return {'rooms': len(scene.rooms), 'beds': beds, 'doors': doors, 'windows': len(scene.windows), 'walking_stairs': len(scene.stairs), 'reachable_cells': len(visited), 'blocks': sum(name_of(s) != 'air' for s in blocks.values()), 'envelope_checked': envelope}
#52 · 2026-09-25T01:20:21.941Z · exit=1 · 14s · python - <<'PY'
python - <<'PY' p='lodge/validation.py' s=open(p).read().replace('from collections import deque, Counter','from collections import deque').replace('from functools import lru_cache','from functools import lru_cache\nimport hashlib\nimport json') a=s.index('\ndef validate_scene(') s=s[:a]+''' def isolated_floor_cells(scene, visited): """Find usable interior floor cells that final furniture or framing isolated.""" missed = set() for v in scene.plan.volumes: for f in v.levels: for x in range(v.x0 + 1, v.x1): for z in range(v.z0 + 1, v.z1): if solid_support(scene.get(x, f, z)) and all(walk_passable(scene.get(x, f + dy, z)) for dy in (1, 2)) and (x, f + 1, z) not in visited: missed.add((x, f + 1, z)) return missed def geometry_signature(scene): """Hash actual roof occupancy and room partitions, ignoring material noise. Args: scene (Scene): Final placed or reloaded geometry. Returns: str: Observed sample signature, not a count of possible designs. """ main = scene.plan.volumes[0] mask = set() for (x, y, z), state in scene.blocks.items(): if 'deepslate' in name_of(state): mask.add((x - main.x0, y - main.base, z - main.z0, 'roof')) for volume in scene.plan.volumes: for floor in volume.levels: for x in range(volume.x0, volume.x1 + 1): for z in range(volume.z0, volume.z1 + 1): # Floor, wall and void geometry only. Decoration colour is irrelevant. if solid_support(scene.get(x, floor, z)): mask.add((x - main.x0, floor - main.base, z - main.z0, 'floor')) if not walk_passable(scene.get(x, floor + 1, z)): mask.add((x - main.x0, floor + 1 - main.base, z - main.z0, 'occupied')) return hashlib.sha256(json.dumps(sorted(mask), separators=(',', ':')).encode()).hexdigest() ''' + s[a:] s=s.replace(" assert beds >= 1, 'no sleeping accommodation'", " missed = isolated_floor_cells(scene, visited)\n assert not missed, f'isolated usable floor cells: {sorted(missed)[:12]}'\n for x, y, z in scene.doors:\n n, props = parse(blocks.get((x, y, z)))\n assert n.endswith('_door'), f'overwritten door {(x, y, z)}'\n dx, dz = DIRECTIONS[props['facing']]\n for sign in (-1, 1):\n assert all(walk_passable(get(x + sign * dx, y + dy, z + sign * dz)) for dy in (0, 1)), f'blocked doorway {(x, y, z)}'\n for v in p.volumes:\n if v.kind != 'dormer':\n assert all(solid_support(get(x, y, z)) for x in range(v.x0, v.x1 + 1) for z in range(v.z0, v.z1 + 1) for y in range(v.base)), f'discontinuous foundation {v.name}'\n assert beds >= 1, 'no sleeping accommodation'") open(p,'w').write(s) p='lodge/plan.py';s=open(p).read().replace(' limits: dict\n', ' limits: dict\n overrides: dict = field(default_factory=dict)\n').replace('trees, origin, size, lim)', 'trees, origin, size, lim, overrides)');open(p,'w').write(s) p='generate.py';s=open(p).read().replace(" record['structural_signature'] = self.plan.signature()", " record['structural_signature'] = self.plan.signature()\n from lodge.validation import geometry_signature\n record['placed_geometry_signature'] = geometry_signature(self.scene)");open(p,'w').write(s) PY cat > test/conftest.py <<'PY' # Keep imports and all ephemeral test output within the delivered workspace. import sys import tempfile from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) @pytest.fixture def artifact_dir(): """Create and clean a scratch directory beneath test/.""" with tempfile.TemporaryDirectory(prefix='artifacts-', dir=ROOT / 'test') as directory: yield Path(directory) PY cat > test/cases.py <<'PY' # Deliberate boundaries and feature intersections supplement the ordinary seed corpus. TARGETS = [ ('small_cottage', {'topology': 'single', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'z', 'profile': 'chalet', 'wing_floors': 1, 'tower': 'none', 'conservatory': False, 'porch': 'full', 'balcony': False, 'dormers': 0, 'terrain': 'garden'}), ('narrow_cross_dormers', {'topology': 'ell', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'x', 'profile': 'chalet', 'wing_size': 9, 'wing_floors': 1, 'tower': 'stone', 'tower_width': 9, 'conservatory': True, 'porch': 'entry', 'balcony': False, 'dormers': 2, 'terrain': 'terrace'}), ('cliff_courtyard', {'topology': 'courtyard', 'width': 19, 'depth': 21, 'floors': 3, 'ridge': 'x', 'profile': 'steep', 'wing_size': 13, 'wing_floors': 2, 'tower': 'stone', 'tower_width': 11, 'tower_extra': 2, 'conservatory': True, 'porch': 'full', 'balcony': True, 'dormers': 2, 'terrain': 'cliff', 'layout': 'suites'}), ('long_inn', {'topology': 'staggered', 'width': 19, 'depth': 21, 'floors': 3, 'ridge': 'z', 'profile': 'bell', 'wing_size': 13, 'wing_floors': 2, 'tower': 'timber', 'tower_width': 11, 'tower_extra': 2, 'conservatory': True, 'porch': 'full', 'balcony': True, 'dormers': 2, 'terrain': 'cliff'}), ('glass_wrap', {'topology': 'ell', 'width': 15, 'depth': 17, 'floors': 2, 'ridge': 'x', 'profile': 'chalet', 'wing_floors': 1, 'tower': 'none', 'conservatory': True, 'porch': 'wrap', 'balcony': True, 'dormers': 2, 'terrain': 'garden', 'entrance': 'left'}), ('short_watchtower', {'topology': 'single', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'z', 'profile': 'chalet', 'wing_floors': 1, 'tower': 'stone', 'tower_width': 11, 'tower_extra': 2, 'conservatory': False, 'porch': 'entry', 'balcony': False, 'dormers': 0, 'terrain': 'garden'}), ('cross_twin', {'topology': 'twin', 'width': 17, 'depth': 19, 'floors': 2, 'ridge': 'x', 'profile': 'bell', 'wing_size': 9, 'wing_floors': 2, 'wing_ridge': 'cross', 'placement': 'front', 'tower': 'none', 'conservatory': False, 'porch': 'entry', 'balcony': False, 'dormers': 1, 'layout': 'open'}), ('rear_gallery', {'topology': 'ell', 'width': 19, 'depth': 21, 'floors': 3, 'ridge': 'z', 'profile': 'steep', 'wing_size': 9, 'wing_floors': 2, 'placement': 'rear', 'tower': 'stone', 'tower_extra': 1, 'conservatory': False, 'porch': 'wrap', 'balcony': True, 'dormers': 2, 'layout': 'suites'}), ] PY cat > test/test_generator.py <<'PY' # Exercise real block geometry over a finite corpus, targeted cases, and deliberate defects. from __future__ import annotations import copy import json import subprocess import sys from pathlib import Path import pytest from generate import Build, generate, blockstates_equivalent from blocks import load_schematic from lodge.plan import DEFAULT_SEED, DOMAINS, resolve from lodge.geometry import DIRECTIONS, blockstate, name_of from lodge.validation import validate_scene, envelope_leaks, geometry_signature, parse, reachable from cases import TARGETS CORPUS = list(range(100)) + [-(2**63), 2**63 - 1, DEFAULT_SEED, 20260923] @pytest.mark.parametrize('seed', CORPUS) def test_seed_corpus(seed): build = generate(seed) scene = build.scene for room in scene.rooms: x0, y0, z0, x1, y1, z1 = room['bounds'] contents = {name_of(state) for (x, y, z), state in scene.blocks.items() if x0 <= x <= x1 and y0 <= y <= y1 and z0 <= z <= z1} if 'kitchen' in room['purpose']: assert 'smoker' in contents and 'crafting_table' in contents, (seed, room) if 'bed' in room['purpose'] or 'suite' in room['purpose']: assert any(name.endswith('_bed') for name in contents), (seed, room) visited = reachable(scene) for foot in scene.beds: x, y, z = foot _, props = parse(scene.blocks[foot]) dx, dz = DIRECTIONS[props['facing']] assert any((xx + a, y, zz + b) in visited for xx, zz in ((x, z), (x + dx, z + dz)) for a, b in DIRECTIONS.values()), (seed, foot) @pytest.mark.parametrize('name,parameters', TARGETS, ids=[n for n, _ in TARGETS]) def test_targeted_geometry(name, parameters): build = generate(907, parameters) assert build.validation['envelope_checked'] assert all(any(r['volume'] == v.name and r['floor'] == f for r in build.scene.rooms) for v in build.plan.volumes for f in v.levels) @pytest.mark.parametrize('seed', [0, 8, 31, DEFAULT_SEED, -(2**63), 2**63 - 1]) def test_determinism_and_semantic_roundtrip(seed, artifact_dir): a = generate(seed, output=artifact_dir / 'a.litematic') b = generate(seed, output=artifact_dir / 'b.litematic') assert a.placed == b.placed assert (artifact_dir / 'a.litematic').read_bytes() == (artifact_dir / 'b.litematic').read_bytes() assert a.plan.record() == b.plan.record() loaded = load_schematic(artifact_dir / 'a.litematic') assert loaded.size_yzx == (a.size_y, a.size_z, a.size_x) ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) ox, oy, oz = a.plan.origin scene = copy.copy(a.scene) scene.blocks = {p: loaded.palette[int(ids[p[1] + oy, p[2] + oz, p[0] + ox])] for p in a.scene.blocks} assert geometry_signature(scene) == geometry_signature(a.scene) validate_scene(scene) record = json.loads((artifact_dir / 'a.json').read_text()) replay = generate(record['seed'], record['overrides']) assert replay.placed == a.placed def test_semantic_property_comparison_and_overwrite(artifact_dir): left = 'minecraft:oak_stairs[facing=north,half=bottom,shape=straight]' reordered = 'minecraft:oak_stairs[shape=straight,half=bottom,facing=north]' assert blockstates_equivalent(left, reordered) assert not blockstates_equivalent(left, left.replace('north', 'south')) assert not blockstates_equivalent(left, 'minecraft:oak_stairs[facing=north,half=bottom]') assert not blockstates_equivalent('minecraft:stone', 'minecraft:cobblestone') build = Build(31).build() build.put(0, 0, 0, 'minecraft:dirt') build.put(0, 0, 0, 'minecraft:oak_stairs', facing='north', half='bottom') build.export(artifact_dir / 'overwrite.litematic') build.verify(artifact_dir / 'overwrite.litematic') with pytest.raises(AssertionError, match='out of bounds'): build.put(-1, 0, 0, 'minecraft:stone') @pytest.mark.parametrize('params', [ {'width': 16}, {'floors': True}, {'unknown': 1}, {'floors': 1, 'wing_floors': 2}, {'floors': 1, 'balcony': True}, {'topology': 'courtyard', 'porch': 'wrap'}, ]) def test_invalid_explicit_parameters(params): with pytest.raises(ValueError): resolve(0, params) @pytest.mark.parametrize('seed', [True, -(2**63) - 1, 2**63, '7']) def test_seed_domain(seed): with pytest.raises(ValueError, match='signed 64-bit'): resolve(seed) def test_resource_limits_are_enforced(): with pytest.raises(ValueError, match='resource limits'): resolve(0, limits={'max_axis': 20}) with pytest.raises(ValueError, match='floor_height'): resolve(0, limits={'floor_height': 5}) with pytest.raises(ValueError, match='unknown resource limit'): resolve(0, limits={'hidden': 100}) with pytest.raises(ValueError, match='not a Java'): blockstate('unobtainium') with pytest.raises(ValueError, match='invalid'): blockstate('oak_stairs', (('facing', 'up'),)) def test_observed_structural_diversity(): signatures = set() coverage = {k: set() for k in ('topology', 'floors', 'ridge', 'profile', 'tower', 'conservatory', 'porch', 'balcony', 'dormers', 'terrain', 'entrance', 'layout', 'stair_side')} for seed in range(40): b = Build(seed).build() signatures.add(geometry_signature(b.scene)) for key in coverage: coverage[key].add(b.plan.parameters[key]) assert len(signatures) == 40 for key, observed in coverage.items(): assert observed == set(DOMAINS[key]), (key, observed) # Change a genuine room-division parameter under the same massing. params = dict(TARGETS[0][1], floors=2) a = Build(31, dict(params, layout='open')).build() b = Build(31, dict(params, layout='suites')).build() assert a.plan.signature() == b.plan.signature() assert geometry_signature(a.scene) != geometry_signature(b.scene) @pytest.mark.parametrize('defect', ['door', 'bed', 'stair_headroom', 'ladder_support', 'window', 'entry']) def test_actual_geometry_defects_are_rejected(defect): scene = Build(DEFAULT_SEED).build().scene if defect == 'door': x, y, z = scene.doors[0] scene.p(x, y + 1, z, 'air') elif defect == 'bed': x, y, z = scene.beds[0] scene.p(x, y, z, 'air') elif defect == 'stair_headroom': x, y, z, _ = scene.stairs[-1] scene.p(x, y + 2, z, 'stone') elif defect == 'ladder_support': x, y, z = scene.ladders[0] _, props = parse(scene.blocks[x, y, z]) dx, dz = DIRECTIONS[props['facing']] scene.p(x - dx, y, z - dz, 'air') elif defect == 'window': scene.p(*scene.windows[0]['cells'][0], 'stone') else: x, y, z = scene.plan.entrance scene.box((x, y, z, x, y + 2, z), 'stone') with pytest.raises(AssertionError): validate_scene(scene) def test_envelope_defect_is_rejected(): scene = Build(907, TARGETS[0][1]).build().scene v = scene.plan.volumes[0] x, z = (v.x0 + v.x1) // 2, (v.z0 + v.z1) // 2 h = v.roof(x, z) scene.clear((x, h - 2, z, x, h + 3, z)) assert envelope_leaks(scene) def test_cli_clean_directory(artifact_dir): script = Path(__file__).resolve().parents[1] / 'generate.py' subprocess.run([sys.executable, str(script), '--seed', '31'], cwd=artifact_dir, check=True, capture_output=True) assert (artifact_dir / 'output.litematic').is_file() assert json.loads((artifact_dir / 'output.json').read_text())['seed'] == 31 PY /opt/venv/bin/python -m pytest -q test --disable-warnings --maxfail=4

记录中的输出末尾

rpose']: > assert 'smoker' in contents and 'crafting_table' in contents, (seed, room) E AssertionError: (26, {'volume': 'main', 'floor': 4, 'purpose': 'hearth kitchen and dining', 'bounds': (1, 5, 1, 17, 8, 15), ...}) E assert ('smoker' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...} and 'crafting_table' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...}) test/test_generator.py:29: AssertionError _____________________________ test_seed_corpus[60] _____________________________ seed = 60 @pytest.mark.parametrize('seed', CORPUS) def test_seed_corpus(seed): build = generate(seed) scene = build.scene for room in scene.rooms: x0, y0, z0, x1, y1, z1 = room['bounds'] contents = {name_of(state) for (x, y, z), state in scene.blocks.items() if x0 <= x <= x1 and y0 <= y <= y1 and z0 <= z <= z1} if 'kitchen' in room['purpose']: > assert 'smoker' in contents and 'crafting_table' in contents, (seed, room) E AssertionError: (60, {'volume': 'main', 'floor': 4, 'purpose': 'hearth kitchen and dining', 'bounds': (1, 5, 1, 15, 8, 19), ...}) E assert ('smoker' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...} and 'crafting_table' in {'air', 'barrel', 'campfire', 'cobblestone', 'iron_bars', 'lantern', ...}) test/test_generator.py:29: AssertionError =========================== short test summary info ============================ FAILED test/test_generator.py::test_seed_corpus[0] - AssertionError: (0, {'vo... FAILED test/test_generator.py::test_seed_corpus[16] - AssertionError: isolate... FAILED test/test_generator.py::test_seed_corpus[26] - AssertionError: (26, {'... FAILED test/test_generator.py::test_seed_corpus[60] - AssertionError: (60, {'... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 4 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 4 failed, 57 passed in 14.05s
#53 · 2026-09-25T01:20:40.913Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' from generate import Build from lodge.validation import reachable,isolated_floor_cells for seed in (0,16,26): s=Build(seed).build().scene print(seed,s.plan.parameters) for r in s.rooms: if 'kitchen' in r['purpose']:print([(q,s.get(*q)) for q in r['furniture']]) if seed==16: print('bad',isolated_floor_cells(s,reachable(s))) v=s.plan.volumes[0];f=v.levels[1];visited=reachable(s) print(v) for z in range(v.z0,v.z1+1):print(z,''.join('.' if (x,f+1,z) in visited else '?' if s.get(x,f+1,z)=='air' and s.get(x,f+2,z)=='air' else s.get(x,f+1,z)[0] for x in range(v.x0,v.x1+1))) PY

记录中的输出末尾

rance': 'left', 'layout': 'open', 'stair_side': 'left', 'palette': 'ochre', 'trees': 4, 'wrap_side': None} [((10, 17, 2), 'barrel'), ((10, 17, 14), 'barrel'), ((4, 17, 14), 'smoker')] 16 {'topology': 'twin', 'width': 17, 'depth': 19, 'floors': 2, 'ridge': 'x', 'profile': 'chalet', 'wing_size': 11, 'wing_floors': 1, 'wing_ridge': 'cross', 'placement': 'rear', 'tower': 'timber', 'tower_extra': 2, 'tower_width': 9, 'conservatory': False, 'porch': 'entry', 'balcony': True, 'dormers': 1, 'terrain': 'terrace', 'approach': 'straight', 'entrance': 'right', 'layout': 'suites', 'stair_side': 'right', 'palette': 'honey', 'trees': 3, 'wrap_side': None} [((4, 8, 2), 'barrel'), ((5, 8, 12), 'barrel'), ((4, 8, 16), 'smoker'), ((1, 8, 1), 'crafting_table'), ((3, 8, 4), 'barrel'), ((3, 8, 7), 'furnace'), ((3, 8, 11), 'bookshelf'), ((3, 8, 14), 'smoker'), ((5, 8, 5), 'crafting_table')] bad {(11, 14, 5), (11, 14, 6), (12, 14, 5)} Volume(name='main', x0=0, z0=0, x1=16, z1=18, base=7, floors=2, axis='x', profile='chalet', kind='house', attic=True, parent=None, floor_step=6) 0 sssssssssss.sssss 1 sc.............bs 2 s...b...........s 3 s...............s 4 s..b......soo...s 5 s....c....s??b..s 6 s.........s?ss..s 7 s..l......sc....s 8 s.........s.....s 9 s...............s 10 s.........s.....s 11 s..c....ssa??...s 12 s....b..ssa??...s 13 s........sssss..s 14 s..b............s 15 s.........s.....s 16 sss.oo....s.b...s 17 sss.......s.....s 18 sssssssssssssssss 26 {'topology': 'ell', 'width': 19, 'depth': 17, 'floors': 2, 'ridge': 'z', 'profile': 'chalet', 'wing_size': 11, 'wing_floors': 1, 'wing_ridge': 'parallel', 'placement': 'rear', 'tower': 'stone', 'tower_extra': 1, 'tower_width': 11, 'conservatory': False, 'porch': 'wrap', 'balcony': False, 'dormers': 2, 'terrain': 'garden', 'approach': 'straight', 'entrance': 'center', 'layout': 'open', 'stair_side': 'right', 'palette': 'ivory', 'trees': 4, 'wrap_side': 'right'} [((2, 5, 4), 'barrel'), ((16, 5, 10), 'barrel'), ((16, 5, 4), 'smoker')]
#54 · 2026-09-25T01:22:05.563Z · exit=0 · 0s · /opt/venv/bin/python /opt/tools/cutaway.py --help && sed -n '10,120p' render.mjs && rg -n 'if len\(spots\)|for
/opt/venv/bin/python /opt/tools/cutaway.py --help && sed -n '10,120p' render.mjs && rg -n 'if len\(spots\)|for x in range\(x0' lodge/geometry.py | tail -7

记录中的输出末尾

{ 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})`); } 74: for x in range(x0, x1 + 1): 81: for x in range(x0, x1 + 1): 547: for x in range(x0, x1 + 1): 673: for x in range(x0 - 1, x1 + 2): 693: for x in range(x0, x1 + 1): 695: for x in range(x0 + 1, x1):
#55 · 2026-09-25T01:22:06.318Z · exit=0 · 37s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read().replace(' if len(spots) < 6:\n', ' if True:\n') s=s.replace(" self.beds.append(pts[0])\n return True", " self.beds.append(pts[0])\n for xx, yy, zz in pts:\n for a, b in DIRECTIONS.values():\n if all(self.get(xx + a, yy + dy, zz + b) == 'air' for dy in (0, 1)):\n self.reserve((xx + a, yy, zz + b))\n return True") s=s.replace(' for offset in (-3, 3):', ' for offset in (() if wants_bed else (-3, 3)):') # Flatten the fallback loop now that it always supplements the perimeter banks. s=s.replace(' if True:\n for x in range(x0, x1 + 1):\n for z in range(z0, z1 + 1):\n y = f + 1\n if all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == \'air\' for dy in (0, 1)) and self.get(x, f, z) != \'air\':\n spots.append((x, y, z))', ' for x in range(x0, x1 + 1):\n for z in range(z0, z1 + 1):\n y = f + 1\n if all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == \'air\' for dy in (0, 1)) and self.get(x, f, z) != \'air\':\n spots.append((x, y, z))') open(p,'w').write(s) PY /opt/venv/bin/python -m pytest -q test --disable-warnings --maxfail=4

记录中的输出末尾

........................................................................ [ 51%] ................................................................... [100%] 139 passed in 37.45s
#56 · 2026-09-25T01:23:51.790Z · exit=0 · 4s · cat > sample_batch.py <<'PY'
cat > sample_batch.py <<'PY' # Rebuild a representative, fully validated artifact set and its replay manifest. from __future__ import annotations import argparse import hashlib import json from pathlib import Path from generate import generate from lodge.plan import DEFAULT_SEED from lodge.validation import geometry_signature SAMPLES = [ ('default', DEFAULT_SEED, {}), ('cliff_courtyard', 0, {}), ('winter_garden_crag', 2, {}), ('terrace_tower', 8, {}), ('three_wing_inn', 13, {}), ('cross_gabled_lodge', 21, {}), ('woodland_house', 31, {}), ('watchtower_inn', 37, {}), ('twin_garden_wings', 20260923, {}), ('compact_cottage', 907, { 'topology': 'single', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'z', 'profile': 'chalet', 'wing_floors': 1, 'tower': 'none', 'conservatory': False, 'porch': 'full', 'balcony': False, 'dormers': 0, 'terrain': 'garden', }), ('cross_roof_conservatory', 907, { 'topology': 'ell', 'width': 15, 'depth': 17, 'floors': 1, 'ridge': 'x', 'profile': 'chalet', 'wing_size': 9, 'wing_floors': 1, 'tower': 'stone', 'tower_width': 9, 'conservatory': True, 'porch': 'entry', 'balcony': False, 'dormers': 2, 'terrain': 'terrace', }), ] def main(): """Export each sample and record parameters, semantic reload checks, and signatures.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--directory', type=Path, default=Path('samples')) args = parser.parse_args() args.directory.mkdir(parents=True, exist_ok=True) entries = [] for name, seed, overrides in SAMPLES: output = args.directory / f'{name}.litematic' build = generate(seed, overrides, output) entry = { 'name': name, 'seed': seed, 'overrides': overrides, 'schematic': output.name, 'resolved_record': output.with_suffix('.json').name, 'parameters': build.plan.parameters, 'size_xyz': build.plan.size_xyz, 'validation': build.validation, 'placed_geometry_signature': geometry_signature(build.scene), 'sha256': hashlib.sha256(output.read_bytes()).hexdigest(), } entries.append(entry) print(f'{name}: seed {seed}, {build.validation["rooms"]} rooms, validated and reloaded', flush=True) manifest = { 'minecraft_version': 'Java 1.21.1', 'default_seed': DEFAULT_SEED, 'command': 'python sample_batch.py', 'samples': entries, 'observed_samples': len(entries), 'observed_geometry_signatures': len({e['placed_geometry_signature'] for e in entries}), 'observed_topologies': sorted({e['parameters']['topology'] for e in entries}), 'signature_method': 'Actual placed slate roof coordinates, floor support cells, and occupied room cells; normalized to the main volume. Material texture choices are collapsed.', } (args.directory / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') if __name__ == '__main__': main() PY python - <<'PY' p='render.mjs';s=open(p).read().replace('process.env.RENDER_SIZE ?? 512', 'process.env.RENDER_SIZE ?? 640');a=s.index('const VIEWS = [');b=s.index('\n];',a)+3;s=s[:a]+'''const VIEWS = [ { name: 'front', azimuth: 210, elevation: 24, radius: 3.0 }, { name: 'rear', azimuth: 35, elevation: 28, radius: 3.0 }, ];''' + s[b:];open(p,'w').write(s) p='lodge/plan.py';s=open(p).read().replace(" if lim['floor_height'] != 6:", " for key in ('max_axis', 'max_height', 'max_volume'):\n if type(lim[key]) is not int or lim[key] <= 0:\n raise ValueError(f'{key} must be a positive integer')\n heights = lim['tree_height']\n if not isinstance(heights, (tuple, list)) or len(heights) != 2 or any(type(h) is not int for h in heights) or not 10 <= heights[0] <= heights[1] <= 32:\n raise ValueError('tree_height must be [minimum, maximum] within 10..32')\n if lim['floor_height'] != 6:");open(p,'w').write(s) PY cat > render_batch.py <<'PY' # Render sample exteriors and useful interior cuts, with a local labelled HTML gallery. from __future__ import annotations import argparse import html import json import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent VITE = Path('/work/generator/MCRender/node_modules/.bin/vite-node') CUTAWAY = Path('/opt/tools/cutaway.py') RENDER_ONE = Path('/opt/tools/render_schematic.mjs') CUTS = {'default', 'cliff_courtyard', 'winter_garden_crag', 'cross_gabled_lodge', 'watchtower_inn', 'compact_cottage', 'cross_roof_conservatory'} def run(command): """Run one renderer process and retain error details without noisy asset logs.""" result = subprocess.run([str(c) for c in command], cwd=ROOT, capture_output=True, text=True) if result.returncode: raise RuntimeError(result.stdout + result.stderr) def main(): """Render all manifest entries and clean temporary cutaway schematics afterward.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--manifest', type=Path, default=ROOT / 'samples/manifest.json') parser.add_argument('--output', type=Path, default=ROOT / 'previews') parser.add_argument('--only', nargs='*', help='Optional sample names to rerender') args = parser.parse_args() manifest = json.loads(args.manifest.read_text()) args.output.mkdir(parents=True, exist_ok=True) sections = [] for sample in manifest['samples']: name = sample['name'] source = args.manifest.parent / sample['schematic'] output = args.output / name images = ['front', 'rear'] + (['ground_cutaway', 'stair_section'] if name in CUTS else []) if args.only is None or name in args.only: output.mkdir(parents=True, exist_ok=True) run(['xvfb-run', '-a', VITE, '--script', ROOT / 'render.mjs', '--', source, output]) if name in CUTS: record = json.loads(source.with_suffix('.json').read_text()) main = record['volumes'][0] ox, _, oz = record['origin'] # Work under test/ and remove the intermediate schematics on success or failure. with tempfile.TemporaryDirectory(prefix='render-cuts-', dir=ROOT / 'test') as directory: sliced = Path(directory) / 'cut.litematic' run([sys.executable, CUTAWAY, '--input', source, '--output', sliced, '--max-y', main['base'] + 3]) run(['xvfb-run', '-a', VITE, '--script', RENDER_ONE, '--', '--input', sliced, '--output', output / 'ground_cutaway.png', '--size', 640, '--azimuth', 210, '--elevation', 70, '--radius', 2.85]) # Keep the side containing the interior flight; look into the exposed section. axis = 'x' if main['axis'] == 'z' else 'z' center = (main[axis + '0'] + main[axis + '1']) // 2 right = record['parameters']['stair_side'] == 'right' cut = center + (1 if right else -1) + (ox if axis == 'x' else oz) command = [sys.executable, CUTAWAY, '--input', source, '--output', sliced, '--axis', axis, '--max', cut] if right: command.append('--keep-above') run(command) azimuth = (250 if right else 70) if axis == 'x' else (190 if right else 10) run(['xvfb-run', '-a', VITE, '--script', RENDER_ONE, '--', '--input', sliced, '--output', output / 'stair_section.png', '--size', 640, '--azimuth', azimuth, '--elevation', 22, '--radius', 3.0]) print(f'rendered {name}: {", ".join(images)}', flush=True) p = sample['parameters'] caption = f"Seed {sample['seed']} | {p['topology']} | {p['floors']} full storeys + attic | {p['profile']} roof | {p['tower']} tower | {p['terrain']}" figures = ''.join(f'<figure><a href="{name}/{image}.png"><img loading="lazy" src="{name}/{image}.png" alt="{html.escape(name + ": " + image)}"></a><figcaption>{image.replace("_", " ")}</figcaption></figure>' for image in images) sections.append(f'<section><h2>{html.escape(name.replace("_", " "))}</h2><p>{html.escape(caption)}</p><div>{figures}</div></section>') page = '''<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Procedural lodges: validation gallery</title><style>body{background:#23282d;color:#eee;font:16px system-ui;margin:2rem auto;max-width:1500px;padding:0 1rem}h1{font-size:2rem}h2{font-size:1.25rem;text-transform:capitalize}section{padding:1rem 0;border-top:1px solid #53606b}section div{display:flex;flex-wrap:wrap}figure{margin:4px;flex:1 1 300px;max-width:48%}img{width:100%;background:#2f343b}figcaption{padding:6px;color:#bdc7ce}p{color:#ccd4db}</style><h1>Procedural lodge gallery</h1><p>Actual schematic geometry. Every sample passed block, circulation, envelope and reload validation. Cutaways expose furniture and stairs; they are inspection artifacts, not playable schematics.</p>''' + ''.join(sections) + '</html>\n' (args.output / 'index.html').write_text(page) if __name__ == '__main__': main() PY /opt/venv/bin/python -m yapf -i generate.py blocks.py lodge/plan.py lodge/geometry.py lodge/validation.py sample_batch.py render_batch.py test/conftest.py test/cases.py test/test_generator.py python generate.py python sample_batch.py

记录中的输出末尾

{ "output": "output.litematic", "seed": 20260924, "bounds": [ 46, 48, 58 ], "validation": { "rooms": 15, "beds": 2, "doors": 2, "windows": 44, "walking_stairs": 51, "reachable_cells": 2131, "blocks": 16666, "envelope_checked": true } } default: seed 20260924, 15 rooms, validated and reloaded cliff_courtyard: seed 0, 10 rooms, validated and reloaded winter_garden_crag: seed 2, 9 rooms, validated and reloaded terrace_tower: seed 8, 6 rooms, validated and reloaded three_wing_inn: seed 13, 12 rooms, validated and reloaded cross_gabled_lodge: seed 21, 11 rooms, validated and reloaded woodland_house: seed 31, 5 rooms, validated and reloaded watchtower_inn: seed 37, 17 rooms, validated and reloaded twin_garden_wings: seed 20260923, 8 rooms, validated and reloaded compact_cottage: seed 907, 3 rooms, validated and reloaded cross_roof_conservatory: seed 907, 9 rooms, validated and reloaded
#57 · 2026-09-25T01:25:01.267Z · exit=0 · 51s · python render_batch.py
python render_batch.py

记录中的输出末尾

rendered default: front, rear, ground_cutaway, stair_section rendered cliff_courtyard: front, rear, ground_cutaway, stair_section rendered winter_garden_crag: front, rear, ground_cutaway, stair_section rendered terrace_tower: front, rear rendered three_wing_inn: front, rear rendered cross_gabled_lodge: front, rear, ground_cutaway, stair_section rendered woodland_house: front, rear rendered watchtower_inn: front, rear, ground_cutaway, stair_section rendered twin_garden_wings: front, rear rendered compact_cottage: front, rear, ground_cutaway, stair_section rendered cross_roof_conservatory: front, rear, ground_cutaway, stair_section
#58 · 2026-09-25T01:25:01.352Z · exit=128 · 0s · rg --files previews | wc -l && rg --files previews | head -18 && git status --short
rg --files previews | wc -l && rg --files previews | head -18 && git status --short

记录中的输出末尾

57 previews/index.html previews/cross_roof_conservatory/stair_section.camera.json previews/cross_roof_conservatory/stair_section.png previews/cross_roof_conservatory/ground_cutaway.camera.json previews/cross_roof_conservatory/ground_cutaway.png previews/cross_roof_conservatory/rear.png previews/cross_roof_conservatory/front.png previews/compact_cottage/stair_section.camera.json previews/compact_cottage/stair_section.png previews/compact_cottage/ground_cutaway.camera.json previews/compact_cottage/ground_cutaway.png previews/compact_cottage/rear.png previews/compact_cottage/front.png previews/cross_gabled_lodge/stair_section.camera.json previews/cross_gabled_lodge/stair_section.png previews/cross_gabled_lodge/ground_cutaway.camera.json previews/cross_gabled_lodge/ground_cutaway.png previews/cross_gabled_lodge/rear.png fatal: not a git repository (or any parent up to mount point /) Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
#59 · 2026-09-25T01:26:55.947Z · exit=0 · 0s · sed -n '1,160p' lodge/plan.py && sed -n '1,100p' generate.py && sed -n '490,670p' lodge/geometry.py
sed -n '1,160p' lodge/plan.py && sed -n '1,100p' generate.py && sed -n '490,670p' lodge/geometry.py

记录中的输出末尾

z) not in self.reserved for dy in (0, 1)): self.p(x, y, z, 'bookshelf') self.lamp(x, y + 1, z) room['furniture'].append((x, y, z)) room['lights'].append((x, y + 1, z)) break if room['lights']: break assert room['lights'], f'no task-light position for {v.name}' self.enclosed.append(target) def chimney(self): """Run a masonry chimney from a supported hearth through the roof shoulder.""" v = self.plan.volumes[0] x, z, f = v.x0 + 1, v.z1 - 2, v.base h = max(v.roof(x, z), v.roof(x + 1, z)) + 5 self.mix((x, f + 1, z, x + 1, h, z + 1), STONE) for yy in (h - 3, h): self.box((x - 1, yy, z - 1, x + 2, yy, z + 2), 'stone_bricks') self.p(x, h + 1, z, 'campfire', lit='true') for xx, zz in ((x - 1, z - 1), (x + 2, z - 1), (x - 1, z + 2), (x + 2, z + 2)): self.p(xx, h + 1, zz, 'stone_brick_wall') self.box((x - 1, h + 2, z - 1, x + 2, h + 2, z + 2), 'stone_bricks') # A contained hearth opens to the room, with a proper masonry hood. for xx in (x, x + 1): self.p(xx, f + 1, z, 'campfire', lit='true') self.p(xx, f + 2, z, 'air') self.p(xx, f + 1, z - 1, 'iron_bars', east='true', west='true') self.p(xx, f + 2, z - 1, 'iron_bars', east='true', west='true') self.box((x, f + 3, z - 1, x + 1, f + 3, z - 1), 'stone_bricks') def exterior_p(self, x, y, z, name, **props): if any(v.contains(x, z) and v.base < y <= v.roof(x, z) + 1 for v in self.plan.volumes): return if (x, y, z) in self.reserved or self.get(x, y, z) != 'air': return self.p(x, y, z, name, **props) def porches(self): """Build post-supported canopies, optional balcony, and an open side gallery.""" p = self.plan
#60 · 2026-09-25T01:27:19.339Z · exit=0 · 38s · python - <<'PY'
python - <<'PY' p='lodge/geometry.py';s=open(p).read() s=s.replace('if any(c in self.reserved for c in cells + inside + outside):', 'if any(c in self.reserved for c in cells):') a=s.index(' def openings(self):');b=s.index(' for link in self.plan.links:',a) s=s[:a]+s[a:b].replace(' def openings(self):',' def glazing(self):')+' def openings(self):\n'+s[b:] s=s.replace(' self.gable_details()\n self.openings()\n self.circulation()\n self.chimney()\n self.porches()\n self.interiors()', ' self.openings()\n self.circulation()\n self.chimney()\n self.porches()\n self.gable_details()\n self.glazing()\n self.interiors()') s=s.replace("side = p['wrap_side'] if isinstance(p, dict) else p.parameters['wrap_side']", "side = p.parameters['wrap_side']") s=s.replace('Stone tower lookout: a glazed room surrounded by a walkable timber gallery.', 'Stone tower lookout: a glazed room with an east balcony and railed ledges.') open(p,'w').write(s) PY /opt/venv/bin/python -m yapf -i lodge/geometry.py /opt/venv/bin/python -m pytest -q test --disable-warnings --maxfail=4

记录中的输出末尾

........................................................................ [ 51%] ................................................................... [100%] 139 passed in 37.39s
#61 · 2026-09-25T01:28:43.442Z · exit=0 · 0s · cat > GENERATOR.md <<'MD'
cat > GENERATOR.md <<'MD' # Procedural woodland lodges A reusable Minecraft Java **1.21.1** generator. It composes connected room stacks, roof envelopes, circulation, extensions and landscapes from a constrained grammar. It does not execute or import any reference generator. ## Run Use the supplied container's Python environment and MCIO installation. No downloads are needed. ```sh python generate.py python generate.py --seed 31 --output my_lodge.litematic python generate.py --seed 21 --set topology=ell --set floors=2 --set wing_floors=1 --set tower=timber python generate.py --seed 907 --config parameters.json --output custom.litematic python sample_batch.py python render_batch.py python -m pytest -q test ``` Without arguments, generation writes **`output.litematic`**, one region, using seed **20260924**. The adjacent `output.json` records the seed, original overrides, resolved parameters, dimensions, volumes, connection interfaces, room purposes, furnishing coordinates and geometry signatures. Each sample has the same pair of files. Export timestamps are zero; placement and palette construction have deterministic order. Identical seed and configuration produce identical schematic bytes in this environment. `--config` accepts an object of parameter overrides, for example: ```json {"topology":"twin","floors":2,"wing_floors":1,"porch":"full","terrain":"terrace"} ``` The Python API exposes both planning and generation: ```python from pathlib import Path from generate import generate from lodge.plan import resolve, DOMAINS, LIMITS plan = resolve(21, {"topology": "ell", "floors": 2}) build = generate(21, {"topology": "ell", "floors": 2}, Path("lodge.litematic")) print(build.plan.record()) print(build.validation) ``` For exact replay of an exported record, use `generate(record["seed"], record["overrides"])`. The resolved record also contains derived fields such as `wrap_side`; it is descriptive rather than an input override dictionary. If custom resource limits were used, pass `limits=record["limits"]` too. The planning and material/detail phases each use a local seeded random source. There is no unseeded randomness, random retry loop, seed-specific patch, or fallback building. ## Architectural parameters Seeds are signed 64-bit integers, including both endpoints. Boolean seeds are rejected. Unspecified choices are sampled uniformly from the listed domains before dependent choices are resolved. Major topologies have equal initial sampling weight; towers, ridge directions, roof profiles and terrain are exercised directly. | Parameter | Domain | Geometry or dependency | | --- | --- | --- | | `topology` | `single`, `ell`, `twin`, `courtyard`, `staggered` | Main stack; one side wing; two offset side wings; two forward wings; or side plus rear wing. | | `width`, `depth` | 15/17/19; 17/19/21 | Main exterior dimensions in blocks. Both ridge directions work with each dimension. | | `floors` | 1, 2, 3 | Full main storeys, plus a furnished vaulted attic. Floors are six blocks apart. | | `ridge` | `x`, `z` | Main ridge direction; also changes local room and stair orientation. | | `profile` | `chalet`, `steep`, `bell` | One-rise, two-rise, or flared lower roof with steeper upper courses. | | `wing_size` | 9, 11, 13 | Side/rear wing width. Side depths additionally sample 11/13/15; rear depths 9/11/13. Courtyard wings have longer forward projections. | | `wing_floors` | 1, 2 | Cannot exceed the main full-storey count. Upper wing levels have supported ladders. | | `wing_ridge` | `parallel`, `cross` | Wing ridges relative to the main roof. | | `placement` | `front`, `middle`, `rear` | Attachment setback. Twin wings add a two-block asymmetry. Courtyard rules replace this with forward setbacks of five or seven blocks. | | `tower` | `none`, `stone`, `timber` | Rear tower, attached to the rear wing when one exists. Stone towers have an east lookout balcony. Timber towers have steeper hipped caps. | | `tower_extra`, `tower_width` | 1/2; 9/11 | Tower storeys above the main full-storey count, capped at five; square tower width. | | `conservatory` | `false`, `true` | Glazed hipped winter garden on the exposed east interface, with planting/work furnishings. Width samples 7/9; depth fits its host. | | `porch` | `entry`, `full`, `wrap` | Seven-block entrance canopy, full front canopy, or front plus side gallery. A wrap needs a free side. | | `balcony` | `false`, `true` | Supported front balcony with a complete upper door; requires at least two full main storeys. | | `dormers` | 0, 1, 2 | Cross-gabled, accessible attic reading bays, on opposing roof slopes. | | `terrain` | `garden`, `terrace`, `cliff` | Occupied base height 4, 7 or 16; connected foundations, retaining work and approach length respond. | | `approach` | `straight`, `landing` | Five-block-wide stair ascent, optionally interrupted by a three-block landing. | | `entrance` | `center`, `left`, `right` | Door and approach offset, including balcony alignment. | | `layout` | `open`, `cross`, `suites` | Open living floors, transverse room divisions with arches, or deeper front rooms with upper privacy doors. | | `stair_side` | `left`, `right` | Two-wide main stair position, side aisles and return landings. | | `palette` | `honey`, `ivory`, `ochre` | Harmonized plaster, timber, floor, glazing and textile families. | | `trees` | 3, 4, 5 | Reserved peripheral conifer sites, plus seeded understory. | Dependencies are implemented in `lodge/plan.py`. Invalid explicit combinations raise `ValueError` with the conflicting parameter and allowed alternatives. The built-in sampler resolves the same constraints without retrying. For example, it narrows wing heights after choosing main height, removes balcony availability for one-storey cottages, and selects a free interface for wraparound galleries. Two side wings rule out a wrap. If an ell, a conservatory and a wrap are requested together, the side wing is placed on the east to preserve the west gallery. Inactive parameters remain in the uniform record schema: wing settings do not change a building without wings; tower settings do not change a building without a tower. They are not counted as structural variation. Random wing handedness, individual lengths, setbacks and tree heights are visible in the resolved volumes/sites. No total design-space size is calculated. Default resource caps are horizontal axes **112**, height **96**, and **900,000 cells**. Bounds are derived before placement from the selected masses, roofs, chimney, trees, gardens and approach, then checked against the caps. The Python `limits` argument can change positive resource caps and the tree-height interval within 10..32; the default tree interval is 15..24. The six-block storey module is fixed. Nothing is silently clipped. ## Design and reference use All eight supplied source snapshots and original images informed the design. They are historical reference material, not runtime dependencies or selected building presets. | Reference | Reachable contribution | | --- | --- | | ref-1 | Tall stacked living volumes, strongly pitched roof, attic bays, low entrance canopy and vertical timber rhythm. | | ref-2 | Broad compound lodge, crossing ridges, offset wings, long porch, domestic room organization. | | ref-3 | Compact hearth house, projecting porch/gallery, stone chimney and connected working rooms. | | ref-4 | Masonry watchtower, upper lookout deck, timber tower cap and terraced approach. | | ref-5 | Elevated crag setting, continuous supported ascent, retaining ledges and planted rock base. | | ref-6 | Courtyard and staggered inn compositions, multiple roof heights, functional wings and rear towers. | | ref-7 | Attached glass winter garden, sealed hipped glazing and planted workroom. | | ref-8 | Tall central gable, lower flanking wings, heraldic cloth, generous approach and upper guest accommodation. | The shared vocabulary is restrained cream or ivory infill, structural timber belts and posts, thick dark slate roofs, masonry foundations, supported warm lanterns and evergreen planting. Cross-gables are unions of envelopes: a low roof cannot replace a higher room wall or cut through an occupied volume. Separate stair, doorway, window and furniture reservations prevent later decoration from occupying required space. Glazing is installed after stairs, chimneys and porches so clear circulation space can still receive windows; actual structures decide which openings fit. `generate.py` is the public entry point. `lodge/plan.py` makes dimensioned architectural decisions. `lodge/geometry.py` realizes terrain, envelopes, interfaces, circulation, furnishings and vegetation. `lodge/validation.py` checks final blocks. `blocks.py` retains the provided bounded placement and deterministic MCIO export/reload helpers. New attachment rules can add volumes/interfaces without adding another complete building script. Main floors use actual supported two-wide stairs with three clear blocks above their treads. Wing and tower ladders have solid backing through their served levels. Room purposes include cooking/dining, parlours, workrooms, libraries, sleeping rooms, tower storage/map rooms and greenhouse work. Furnishing density is bounded; bed access, stair returns and dormer approaches remain open. Window glass and both sides of every opening are checked after furnishing. ## Validation and artifacts Run `python -m pytest -q test`. The suite covers **104 seeds** (0..99, signed seed endpoints, the default, and 20260923), **eight targeted configurations**, byte determinism, semantic round trips, resource/parameter rejection and a clean-directory CLI invocation. It checks final geometry, not just declared room edges: - Vanilla block names, explicit property domains and export bounds. - Door/bed pairs, ladder backing, lantern/plant attachment, stair supports, foundations and clear openings. - Exterior-to-room traversal with two-cell body clearance, oriented stair ascent/descent, operable doors and ladders; every usable interior floor cell is checked, including dormer bays. - Room lighting near the floor, sleeping furniture with access, and actual cooking/crafting equipment in kitchens. - Exterior-air flood against the closed envelope. Slate stairs/slabs, fences, plants and other porous features do not count as full roof seals. - Deliberately broken doors, beds, stair headroom, ladder support, windows, entrances and roofs must be rejected. Round-trip comparisons use `blockstates_equivalent`, comparing parsed names and property mappings rather than property order. Several exported/reloaded scenes repeat the physical validation. Temporary test files and render-cut schematics live beneath `test/` and are removed automatically. The 40-seed structural audit observes **40 distinct final geometry signatures** and covers every major branch. Signatures use actual roof occupancy, floor support and room obstruction coordinates, with material noise collapsed. The delivered **11 samples** have **11 distinct observed geometry signatures**. These are finite observed results, not exhaustive coverage or a proof over every possible seed. [`samples/manifest.json`](samples/manifest.json) records exact replay inputs, resolved choices, validation results and file digests. `python sample_batch.py` rebuilds that batch. The examples mix features across the reference family: cliff courtyards, conservatories on tower cottages, staggered watchtower inns, small woodland cottages and tall flanked lodges. [`previews/index.html`](previews/index.html) is the labelled gallery. `python render_batch.py` produces front/rear views for every sample and ground-floor/stair cutaways for seven representative samples. Source paths, seeds and architectural labels are in the manifest/gallery. The images render the actual schematics; cutaways are inspection views, not additional playable outputs. ## Practical limits Validation is bounded and repeatable, not exhaustive. The traversal model conservatively treats two-block air/door/ladder columns as usable and explicitly checks stair directions; it is not a running Minecraft server or full player collision simulator. Shader lighting, smoke particles and block-entity rendering are outside the supplied renderer. Furnishings use vanilla block states; container inventories, lectern books and custom banner NBT are not populated. The grammar uses orthogonal rectangular volumes, fixed-height floors, straight interior flights and straight or landing-interrupted outdoor ascents. It does not generate curved walls, spiral stairs or cliff switchbacks. Main attics are vaulted rooms; tower heights and roof profiles stay within the bounded lodge scale. Ground-level interfaces connect wings; independent ladders serve their upper floors. These are implemented scope limits, not unfinished export or validation work. MD python - <<'PY' from pathlib import Path import shutil old=Path('previews/initial') if old.exists():shutil.rmtree(old) for name in ('side.png','top.png'): p=Path('previews/default')/name if p.exists():p.unlink() PY

记录中的输出末尾

#62 · 2026-09-25T01:29:12.968Z · exit=0 · 0s · sed -n '1,190p' /opt/tools/cutaway.py
sed -n '1,190p' /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()
#63 · 2026-09-25T01:29:53.370Z · exit=0 · 54s · python generate.py && python sample_batch.py && python render_batch.py
python generate.py && python sample_batch.py && python render_batch.py

记录中的输出末尾

{ "output": "output.litematic", "seed": 20260924, "bounds": [ 46, 48, 58 ], "validation": { "rooms": 15, "beds": 2, "doors": 2, "windows": 68, "walking_stairs": 51, "reachable_cells": 2123, "blocks": 16787, "envelope_checked": true } } default: seed 20260924, 15 rooms, validated and reloaded cliff_courtyard: seed 0, 10 rooms, validated and reloaded winter_garden_crag: seed 2, 9 rooms, validated and reloaded terrace_tower: seed 8, 6 rooms, validated and reloaded three_wing_inn: seed 13, 12 rooms, validated and reloaded cross_gabled_lodge: seed 21, 11 rooms, validated and reloaded woodland_house: seed 31, 5 rooms, validated and reloaded watchtower_inn: seed 37, 17 rooms, validated and reloaded twin_garden_wings: seed 20260923, 8 rooms, validated and reloaded compact_cottage: seed 907, 3 rooms, validated and reloaded cross_roof_conservatory: seed 907, 9 rooms, validated and reloaded rendered default: front, rear, ground_cutaway, stair_section rendered cliff_courtyard: front, rear, ground_cutaway, stair_section rendered winter_garden_crag: front, rear, ground_cutaway, stair_section rendered terrace_tower: front, rear rendered three_wing_inn: front, rear rendered cross_gabled_lodge: front, rear, ground_cutaway, stair_section rendered woodland_house: front, rear rendered watchtower_inn: front, rear, ground_cutaway, stair_section rendered twin_garden_wings: front, rear rendered compact_cottage: front, rear, ground_cutaway, stair_section rendered cross_roof_conservatory: front, rear, ground_cutaway, stair_section
#64 · 2026-09-25T01:30:42.523Z · exit=0 · 9s · python - <<'PY'
python - <<'PY' p='render_batch.py';s=open(p).read().replace("parser.add_argument('--only', nargs='*', help='Optional sample names to rerender')", "parser.add_argument('--only', nargs='*', help='Optional sample names to rerender')\n parser.add_argument('--cuts-only', action='store_true', help='Refresh cutaways while retaining existing exterior renders')").replace(" run(['xvfb-run', '-a', VITE, '--script', ROOT / 'render.mjs', '--', source, output])", " if not args.cuts_only:\n run(['xvfb-run', '-a', VITE, '--script', ROOT / 'render.mjs', '--', source, output])").replace("center + (1 if right else -1)", "center + (1 if right else -2)");open(p,'w').write(s) p='blocks.py';s=open(p).read().replace('import argparse\n','').replace('"seed": 20260923','"seed": 20260924');a=s.index(' def build(self) -> "Build":');b=s.index(' def export(',a);s=s[:a]+s[b:];a=s.index('\ndef main() -> None:');s=s[:a];open(p,'w').write(s) p='lodge/plan.py';s=open(p).read().replace('from typing import Any\n','');open(p,'w').write(s) p='test/test_generator.py';s=open(p).read().replace('import copy\n','import copy\nimport gzip\n').replace('from blocks import load_schematic','from blocks import load_schematic\nfrom mcio.nbt.nbt import File as NBTFile') s=s.replace(" loaded = load_schematic(artifact_dir / 'a.litematic')", " with gzip.open(artifact_dir / 'a.litematic', 'rb') as stream:\n root = NBTFile.from_buffer(stream).root\n assert len(root['Regions']) == 1\n assert int(root['Metadata']['TimeCreated']) == int(root['Metadata']['TimeModified']) == 0\n loaded = load_schematic(artifact_dir / 'a.litematic')") s=s.replace("def test_resource_limits_are_enforced():", "def test_resource_limits_are_enforced():\n for limits in ({'max_axis': False}, {'max_height': -1}, {'tree_height': [4, 50]}, {'tree_height': [25, 20]}):\n with pytest.raises(ValueError):\n resolve(0, limits=limits)") open(p,'w').write(s) PY python -m yapf -i blocks.py lodge/plan.py render_batch.py test/test_generator.py python -m pytest -q test/test_generator.py -k 'determinism or resource or cli or semantic' --disable-warnings python render_batch.py --cuts-only --only default

记录中的输出末尾

/ 'b.litematic').read_bytes() assert a.plan.record() == b.plan.record() with gzip.open(artifact_dir / 'a.litematic', 'rb') as stream: > root = NBTFile.from_buffer(stream).root ^^^^^^^^^^^^^^^^^^^ E AttributeError: type object 'File' has no attribute 'from_buffer' test/test_generator.py:57: AttributeError _________ test_determinism_and_semantic_roundtrip[9223372036854775807] _________ seed = 9223372036854775807 artifact_dir = PosixPath('/work/generator/workspace/test/artifacts-pis62pt0') @pytest.mark.parametrize('seed', [0, 8, 31, DEFAULT_SEED, -(2**63), 2**63 - 1]) def test_determinism_and_semantic_roundtrip(seed, artifact_dir): a = generate(seed, output=artifact_dir / 'a.litematic') b = generate(seed, output=artifact_dir / 'b.litematic') assert a.placed == b.placed assert (artifact_dir / 'a.litematic').read_bytes() == (artifact_dir / 'b.litematic').read_bytes() assert a.plan.record() == b.plan.record() with gzip.open(artifact_dir / 'a.litematic', 'rb') as stream: > root = NBTFile.from_buffer(stream).root ^^^^^^^^^^^^^^^^^^^ E AttributeError: type object 'File' has no attribute 'from_buffer' test/test_generator.py:57: AttributeError =========================== short test summary info ============================ FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[0] - A... FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[8] - A... FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[31] - ... FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[20260924] FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[-9223372036854775808] FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[9223372036854775807] 6 failed, 4 passed, 129 deselected in 5.01s rendered default: front, rear, ground_cutaway, stair_section
#65 · 2026-09-25T01:31:29.933Z · exit=0 · 0s · sed -n '1,230p' /work/generator/MCIO/mcio/nbt/nbt.py
sed -n '1,230p' /work/generator/MCIO/mcio/nbt/nbt.py

记录中的输出末尾

g.Compound.parse` classmethod. .. doctest:: >>> fileobj.seek(0) 0 >>> nbtlib.File.parse(fileobj) == nbt_file True Attributes: filename: The name of the file, ``None`` by default. The attribute is set automatically when the file is returned from the :func:`load` helper function and can also be set in the constructor. .. doctest:: >>> nbt_file.filename is None True >>> nbtlib.load("docs/demo.nbt").filename 'docs/demo.nbt' gzipped: Boolean indicating if the file is gzipped. The attribute can also be set in the constructor. New files are uncompressed by default. .. doctest:: >>> nbtlib.File(nbt_file, gzipped=True).gzipped True byteorder: The byte order, either ``"big"`` or ``"little"``. The attribute can also be set in the constructor. New files are big-endian by default. .. doctest:: >>> nbtlib.File(nbt_file, byteorder="little").byteorder 'little' """ def __init__(self, *args, gzipped: bool = False, byteorder: ByteOrder = ByteOrder.BIG, filename=None, root_name=""): super().__init__(*args) self.filename = filename self.gzipped = gzipped self.byteorder = byteorder self.root_name = root_name @classmethod def parse(cls, fileobj, byteorder: ByteOrder = ByteOrder.BIG): """Override :meth:`nbtlib.tag.Base.parse` for nbt files.""" tag_id = read_numeric(BYTE, fileobj, byteorder) if not tag_id == cls.tag_id: raise TypeError(f"Non-Compound root tags is not supported: {cls.get_tag(tag_id)}") name = read_string(fileobj, byteorder) self = super().parse(fileobj, byteorder) self.root_name = name return self
#66 · 2026-09-25T01:32:40.189Z · exit=0 · 31s · python - <<'PY'
python - <<'PY' p='test/test_generator.py';s=open(p).read().replace('NBTFile.from_buffer(stream).root','NBTFile.from_fileobj(stream)');open(p,'w').write(s) PY python -m yapf -i test/test_generator.py python -m pytest -q test/test_generator.py -k 'determinism or resource or cli or semantic' --disable-warnings python render_batch.py --cuts-only --only cliff_courtyard winter_garden_crag cross_gabled_lodge watchtower_inn compact_cottage cross_roof_conservatory

记录中的输出末尾

2/gzip.py:544: in read if not self._read_gzip_header(): ^^^^^^^^^^^^^^^^^^^^^^^^ /opt/uv-python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/gzip.py:513: in _read_gzip_header last_mtime = _read_gzip_header(self._fp) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ fp = <gzip._PaddedFile object at 0x7fca71bf4830> def _read_gzip_header(fp): '''Read a gzip header from `fp` and progress to the end of the header. Returns last mtime if header was present or None otherwise. ''' magic = fp.read(2) if magic == b'': return None if magic != b'\037\213': > raise BadGzipFile('Not a gzipped file (%r)' % magic) E gzip.BadGzipFile: Not a gzipped file (b'\n\x00') /opt/uv-python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/gzip.py:473: BadGzipFile =========================== short test summary info ============================ FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[0] - g... FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[8] - g... FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[31] - ... FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[20260924] FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[-9223372036854775808] FAILED test/test_generator.py::test_determinism_and_semantic_roundtrip[9223372036854775807] 6 failed, 4 passed, 129 deselected in 5.18s rendered cliff_courtyard: front, rear, ground_cutaway, stair_section rendered winter_garden_crag: front, rear, ground_cutaway, stair_section rendered cross_gabled_lodge: front, rear, ground_cutaway, stair_section rendered watchtower_inn: front, rear, ground_cutaway, stair_section rendered compact_cottage: front, rear, ground_cutaway, stair_section rendered cross_roof_conservatory: front, rear, ground_cutaway, stair_section
#67 · 2026-09-25T01:33:21.340Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' import json,hashlib from pathlib import Path from generate import Build from lodge.validation import geometry_signature manifest=json.loads(Path('samples/manifest.json').read_text()) for entry in manifest['samples']: p=Path('samples')/entry['schematic'] assert hashlib.sha256(p.read_bytes()).hexdigest()==entry['sha256'] record=json.loads(p.with_suffix('.json').read_text()) assert record['placed_geometry_signature']==entry['placed_geometry_signature'] for image in ('front.png','rear.png'): assert (Path('previews')/entry['name']/image).is_file() assert Path('output.litematic').read_bytes()==Path('samples/default.litematic').read_bytes() b=Build().build() assert b.plan.parameters==json.loads(Path('output.json').read_text())['parameters'] assert geometry_signature(b.scene)==manifest['samples'][0]['placed_geometry_signature'] print(json.dumps({'samples':len(manifest['samples']), 'observed_geometry_signatures':manifest['observed_geometry_signatures'], 'schematics_bytes':sum(p.stat().st_size for p in Path('samples').glob('*.litematic')), 'preview_images':len(list(Path('previews').rglob('*.png'))), 'default_matches_sample':True},indent=2)) print('Test directory entries:',sorted(p.name for p in Path('test').iterdir())) PY

记录中的输出末尾

{ "samples": 11, "observed_geometry_signatures": 11, "schematics_bytes": 1192878, "preview_images": 36, "default_matches_sample": true } Test directory entries: ['__pycache__', 'cases.py', 'conftest.py', 'test_generator.py']
#68 · 2026-09-25T01:33:45.728Z · exit=0 · 1s · python - <<'PY'
python - <<'PY' p='test/test_generator.py';s=open(p).read().replace('import gzip\n','').replace('from mcio.nbt.nbt import File as NBTFile','from mcio.nbt.nbt import load as load_nbt').replace(" with gzip.open(artifact_dir / 'a.litematic', 'rb') as stream:\n root = NBTFile.from_fileobj(stream)", " root = load_nbt(artifact_dir / 'a.litematic')");open(p,'w').write(s) PY python -m pytest -q 'test/test_generator.py::test_determinism_and_semantic_roundtrip[0]' --disable-warnings

记录中的输出末尾

. [100%] 1 passed in 1.47s
#69 · 2026-09-25T01:34:40.490Z · exit=0 · 0s · python - <<'PY'
python - <<'PY' p='GENERATOR.md';s=open(p).read().replace('Each sample has the same pair of files.', 'Room and interface coordinates are in planning space; add the recorded `origin` to obtain schematic cell indices. Each sample has the same pair of files.');open(p,'w').write(s) PY cat > previews/REVIEW.md <<'MD' # Render inspection These previews were regenerated from the schematics listed in `samples/manifest.json`, after the final architectural fixes. Open `index.html` for the labelled gallery: 22 exterior views and 14 cutaways across 11 samples. Inspected the compact cottage, tall framed lodge, cliff courtyard, cross-gabled wings, winter garden, watchtower inn and staggered rear composition from their exterior views. Ground cuts expose the kitchen/work furnishings and protected crossing routes. Vertical cuts follow the main stair side and expose the served floors, attic and adjacent tower rooms. Inspection led to three shared-rule fixes: - Glazing is installed after stairs, chimney and porch construction. Clear route reservations no longer suppress usable facade windows. All glazing still receives a final obstruction check. - Furniture banks are sparse, supplemented by additional safe sites when needed. Bed access, gable-end passages, stair return aisles and dormer approaches are reserved. The validator now checks every usable interior floor cell, beyond the declared room targets. - Tower ledges and porch caps cannot replace higher room envelopes. Chimney caps remain solid where they meet another roof or wall. Exterior-air flooding confirms closure. The section camera cuts at the inner tread edge, so the stair slope and its landings are visible. Cutaways deliberately remove enclosure blocks and retain geometry only. Original complete schematics are the playable artifacts and the inputs to validation. Visual limits: the supplied software renderer does not show Minecraft shader lighting, campfire smoke or all block-entity features. Light placement, paired beds/doors and supports are checked in the schematic data. The dataset is a representative batch; all ordinary corpus failures found during development were repaired in common rules and retained in the regression suite. MD rg --files -g 'test/artifacts-*' -g 'test/render-cuts-*' . | wc -l

记录中的输出末尾

0
#70 · 2026-09-25T01:35:16.914Z · exit=0 · 37s · python -m yapf -i test/test_generator.py
python -m yapf -i test/test_generator.py python -m pytest -q test --disable-warnings

记录中的输出末尾

........................................................................ [ 51%] ................................................................... [100%] 139 passed in 37.28s
#71 · 2026-09-25T01:37:52.403Z · exit=0 · 0s · ls -l output.litematic GENERATOR.md samples/manifest.json previews/index.html previews/REVIEW.md
ls -l output.litematic GENERATOR.md samples/manifest.json previews/index.html previews/REVIEW.md

记录中的输出末尾

-rw-rw-r-- 1 agent agent 12968 Sep 25 01:34 GENERATOR.md -rw-rw-r-- 1 agent agent 121873 Sep 25 01:28 output.litematic -rw-rw-r-- 1 agent agent 1903 Sep 25 01:34 previews/REVIEW.md -rw-rw-r-- 1 agent agent 8871 Sep 25 01:32 previews/index.html -rw-rw-r-- 1 agent agent 16150 Sep 25 01:29 samples/manifest.json
#72 · 2026-09-25T01:37:59.707Z · exit=0 · 0s · tail -65 GENERATOR.md
tail -65 GENERATOR.md

记录中的输出末尾

use actual roof occupancy, floor support and room obstruction coordinates, with material noise collapsed. The delivered **11 samples** have **11 distinct observed geometry signatures**. These are finite observed results, not exhaustive coverage or a proof over every possible seed. [`samples/manifest.json`](samples/manifest.json) records exact replay inputs, resolved choices, validation results and file digests. `python sample_batch.py` rebuilds that batch. The examples mix features across the reference family: cliff courtyards, conservatories on tower cottages, staggered watchtower inns, small woodland cottages and tall flanked lodges. [`previews/index.html`](previews/index.html) is the labelled gallery. `python render_batch.py` produces front/rear views for every sample and ground-floor/stair cutaways for seven representative samples. Source paths, seeds and architectural labels are in the manifest/gallery. The images render the actual schematics; cutaways are inspection views, not additional playable outputs. ## Practical limits Validation is bounded and repeatable, not exhaustive. The traversal model conservatively treats two-block air/door/ladder columns as usable and explicitly checks stair directions; it is not a running Minecraft server or full player collision simulator. Shader lighting, smoke particles and block-entity rendering are outside the supplied renderer. Furnishings use vanilla block states; container inventories, lectern books and custom banner NBT are not populated. The grammar uses orthogonal rectangular volumes, fixed-height floors, straight interior flights and straight or landing-interrupted outdoor ascents. It does not generate curved walls, spiral stairs or cliff switchbacks. Main attics are vaulted rooms; tower heights and roof profiles stay within the bounded lodge scale. Ground-level interfaces connect wings; independent ladders serve their upper floors. These are implemented scope limits, not unfinished export or validation work.
#73 · 2026-09-25T01:37:59.753Z · exit=0 · 0s · cat previews/REVIEW.md
cat previews/REVIEW.md

记录中的输出末尾

# Render inspection These previews were regenerated from the schematics listed in `samples/manifest.json`, after the final architectural fixes. Open `index.html` for the labelled gallery: 22 exterior views and 14 cutaways across 11 samples. Inspected the compact cottage, tall framed lodge, cliff courtyard, cross-gabled wings, winter garden, watchtower inn and staggered rear composition from their exterior views. Ground cuts expose the kitchen/work furnishings and protected crossing routes. Vertical cuts follow the main stair side and expose the served floors, attic and adjacent tower rooms. Inspection led to three shared-rule fixes: - Glazing is installed after stairs, chimney and porch construction. Clear route reservations no longer suppress usable facade windows. All glazing still receives a final obstruction check. - Furniture banks are sparse, supplemented by additional safe sites when needed. Bed access, gable-end passages, stair return aisles and dormer approaches are reserved. The validator now checks every usable interior floor cell, beyond the declared room targets. - Tower ledges and porch caps cannot replace higher room envelopes. Chimney caps remain solid where they meet another roof or wall. Exterior-air flooding confirms closure. The section camera cuts at the inner tread edge, so the stair slope and its landings are visible. Cutaways deliberately remove enclosure blocks and retain geometry only. Original complete schematics are the playable artifacts and the inputs to validation. Visual limits: the supplied software renderer does not show Minecraft shader lighting, campfire smoke or all block-entity features. Light placement, paired beds/doors and supports are checked in the schematic data. The dataset is a representative batch; all ordinary corpus failures found during development were repaired in common rules and retained in the regression suite.
#74 · 2026-09-25T01:38:23.707Z · exit=0 · 0s · head -55 GENERATOR.md
head -55 GENERATOR.md

记录中的输出末尾

, "floors": 2}) build = generate(21, {"topology": "ell", "floors": 2}, Path("lodge.litematic")) print(build.plan.record()) print(build.validation) ``` For exact replay of an exported record, use `generate(record["seed"], record["overrides"])`. The resolved record also contains derived fields such as `wrap_side`; it is descriptive rather than an input override dictionary. If custom resource limits were used, pass `limits=record["limits"]` too. The planning and material/detail phases each use a local seeded random source. There is no unseeded randomness, random retry loop, seed-specific patch, or fallback building. ## Architectural parameters Seeds are signed 64-bit integers, including both endpoints. Boolean seeds are rejected. Unspecified choices are sampled uniformly from the listed domains before dependent choices are resolved. Major topologies have equal initial sampling weight; towers, ridge directions, roof profiles and terrain are exercised directly. | Parameter | Domain | Geometry or dependency | | --- | --- | --- | | `topology` | `single`, `ell`, `twin`, `courtyard`, `staggered` | Main stack; one side wing; two offset side wings; two forward wings; or side plus rear wing. | | `width`, `depth` | 15/17/19; 17/19/21 | Main exterior dimensions in blocks. Both ridge directions work with each dimension. | | `floors` | 1, 2, 3 | Full main storeys, plus a furnished vaulted attic. Floors are six blocks apart. | | `ridge` | `x`, `z` | Main ridge direction; also changes local room and stair orientation. | | `profile` | `chalet`, `steep`, `bell` | One-rise, two-rise, or flared lower roof with steeper upper courses. | | `wing_size` | 9, 11, 13 | Side/rear wing width. Side depths additionally sample 11/13/15; rear depths 9/11/13. Courtyard wings have longer forward projections. | | `wing_floors` | 1, 2 | Cannot exceed the main full-storey count. Upper wing levels have supported ladders. | | `wing_ridge` | `parallel`, `cross` | Wing ridges relative to the main roof. |
#75 · 2026-09-25T01:38:29.911Z · exit=0 · 37s · python -m pytest -q test --disable-warnings
python -m pytest -q test --disable-warnings

记录中的输出末尾

........................................................................ [ 51%] ................................................................... [100%] 139 passed in 37.28s

13 · 源码与归因

下表是交付文件的实际规模,排除八份输入参考代码,不能直接解释为全部新写代码。 blocks.py 含模板派生内容。harness 按文件名匹配模板,把嵌套目录里的八份 generate.py 也标成 edited;逐字节核对显示它们与输入参考相同,因此本报告不将它们记作新增成果。

文件行数字节SHA-256
blocks.py2108474444d9f9e3bd46162…
generate.py873568e2ddc40c9869204b…
lodge/geometry.py845456289b4a7c00813e167e…
lodge/plan.py2641176193870464884ebc34…
lodge/validation.py25111369deffd83bb88c0c89…
render_batch.py765399d63e946201a522bc…
sample_batch.py96325894a75ff6f73f0b86…
test/cases.py1413437a5678271967ca91b…
test/conftest.py15438d141ff11de1fbed7…
test/test_generator.py195798111e999a04907dcfc…
generate.py
# Generate reproducible furnished Minecraft Java 1.21.1 lodges from a constrained grammar. from __future__ import annotations import argparse import json from pathlib import Path from blocks import Build as BlockBuild, CONFIG, blockstates_equivalent from lodge.plan import DEFAULT_SEED, DOMAINS, LIMITS, Plan, resolve from lodge.geometry import Scene class Build(BlockBuild): """Compile a resolved architectural plan using the toolset export contract.""" def __init__(self, seed: int = DEFAULT_SEED, parameters: dict | None = None, limits: dict | None = None): self.plan = resolve(seed, parameters, limits) super().__init__(self.plan.size_xyz, seed) self.scene = None def build(self): self.scene = Scene(self.plan).build() ox, oy, oz = self.plan.origin materials = {} for (x, y, z), state in sorted(self.scene.blocks.items()): if state not in materials: materials[state] = self.material(state) self.put(x + ox, y + oy, z + oz, materials[state]) return self def export(self, path: Path): super().export(path) record = self.plan.record() record['rooms'] = self.scene.rooms record['structural_signature'] = self.plan.signature() from lodge.validation import geometry_signature record['placed_geometry_signature'] = geometry_signature(self.scene) path.with_suffix('.json').write_text(json.dumps(record, indent=2) + '\n') return path def generate(seed: int = DEFAULT_SEED, parameters: dict | None = None, output: Path | None = None, validate: bool = True, limits: dict | None = None) -> Build: """Compile, validate, and optionally export one reproducible architectural sample. Args: seed (int): Signed 64-bit seed, default 20260924. parameters (dict): Explicit architectural constraints from DOMAINS. output (Path): Optional schematic path, with adjacent resolved JSON. validate (bool): Run geometry checks before export. limits (dict): Optional documented resource limits. Returns: Build: Blocks, resolved plan, and verification evidence. """ build = Build(seed, parameters, limits).build() if validate: from lodge.validation import validate_scene build.validation = validate_scene(build.scene) if output is not None: build.export(Path(output)) build.verify(Path(output)) return build def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--seed', type=int, default=DEFAULT_SEED) parser.add_argument('--output', type=Path, default=Path('output.litematic')) parser.add_argument('--config', type=Path, help='JSON architectural parameter overrides') parser.add_argument('--set', action='append', default=[], metavar='KEY=JSON', help='Override a parameter, e.g. --set floors=2 --set topology=ell') args = parser.parse_args() params = json.loads(args.config.read_text()) if args.config else {} for setting in args.set: key, value = setting.split('=', 1) try: params[key] = json.loads(value) except json.JSONDecodeError: params[key] = value try: build = generate(args.seed, params, args.output) except ValueError as error: parser.error(str(error)) print(json.dumps({'output': str(args.output), 'seed': args.seed, 'bounds': build.plan.size_xyz, 'validation': build.validation}, indent=2)) if __name__ == '__main__': main()
lodge/plan.py
# Resolve a constrained lodge grammar into connected, dimensioned volumes. from __future__ import annotations from dataclasses import asdict, dataclass, field import random DOMAINS = { 'topology': ('single', 'ell', 'twin', 'courtyard', 'staggered'), 'width': (15, 17, 19), 'depth': (17, 19, 21), 'floors': (1, 2, 3), 'ridge': ('z', 'x'), 'profile': ('chalet', 'steep', 'bell'), 'wing_size': (9, 11, 13), 'wing_floors': (1, 2), 'wing_ridge': ('parallel', 'cross'), 'placement': ('front', 'middle', 'rear'), 'tower': ('none', 'stone', 'timber'), 'tower_extra': (1, 2), 'tower_width': (9, 11), 'conservatory': (False, True), 'porch': ('entry', 'full', 'wrap'), 'balcony': (False, True), 'dormers': (0, 1, 2), 'terrain': ('garden', 'terrace', 'cliff'), 'approach': ('straight', 'landing'), 'entrance': ('center', 'left', 'right'), 'layout': ('open', 'cross', 'suites'), 'stair_side': ('left', 'right'), 'palette': ('honey', 'ivory', 'ochre'), 'trees': (3, 4, 5), } LIMITS = {'max_axis': 112, 'max_height': 96, 'max_volume': 900000, 'floor_height': 6, 'tree_height': (15, 24)} DEFAULT_SEED = 20260924 @dataclass class Volume: """A connected room stack, with a roof in local cross-ridge coordinates.""" name: str x0: int z0: int x1: int z1: int base: int floors: int axis: str = 'z' profile: str = 'chalet' kind: str = 'house' attic: bool = False parent: str | None = None floor_step: int = 6 @property def levels(self): return [self.base + i * self.floor_step for i in range(self.floors + int(self.attic))] @property def uc(self): return (self.x0 + self.x1) // 2 if self.axis == 'z' else (self.z0 + self.z1) // 2 @property def u0(self): return self.x0 if self.axis == 'z' else self.z0 @property def u1(self): return self.x1 if self.axis == 'z' else self.z1 @property def v0(self): return self.z0 if self.axis == 'z' else self.x0 @property def v1(self): return self.z1 if self.axis == 'z' else self.x1 def xyz(self, u, y, v): return (u, y, v) if self.axis == 'z' else (v, y, u) def uv(self, x, z): return (x, z) if self.axis == 'z' else (z, x) def contains(self, x, z, margin=0): return self.x0 - margin <= x <= self.x1 + margin and self.z0 - margin <= z <= self.z1 + margin def roof(self, x, z): u, _ = self.uv(x, z) d = (self.u1 - self.u0) // 2 - abs(u - self.uc) eave = self.levels[-1] + (2 if self.attic else 5) if self.kind == 'glass': return eave + min(x - self.x0, self.x1 - x, z - self.z0, self.z1 - z, 3) if self.kind in ('stone', 'timber'): d = min(x - self.x0, self.x1 - x, z - self.z0, self.z1 - z) eave += 1 rise = d if self.profile == 'chalet' or d < 0 else 2 * d if self.profile == 'steep' else d + max(0, d - 2) return eave + rise def interior(self, x, y, z): return self.x0 < x < self.x1 and self.z0 < z < self.z1 and self.base < y < self.roof(x, z) @dataclass class Plan: seed: int parameters: dict volumes: list[Volume] links: list[dict] entrance: tuple porch: tuple terrace_front: int approach: list[dict] tree_sites: list[tuple] origin: tuple size_xyz: tuple limits: dict overrides: dict = field(default_factory=dict) def record(self): return asdict(self) def signature(self): """Describe actual relative massing and roof structure, independent of seed.""" m = self.volumes[0] return tuple((v.x0 - m.x0, v.z0 - m.z0, v.x1 - v.x0, v.z1 - v.z0, tuple(y - m.base for y in v.levels), v.axis, v.profile, v.kind) for v in self.volumes) def resolve(seed: int, overrides: dict | None = None, limits: dict | None = None) -> Plan: """Resolve one seed and optional architectural constraints without retries. Args: seed (int): Any signed 64-bit integer. overrides (dict): Explicit values from DOMAINS. limits (dict): Resource limits, constrained to the documented keys. Returns: Plan: Dimensioned, connected design with computed export bounds. """ if type(seed) is not int or not -(2**63) <= seed < 2**63: raise ValueError('seed must be a signed 64-bit integer') overrides = dict(overrides or {}) for key, value in overrides.items(): if key not in DOMAINS or value not in DOMAINS[key] or type(value) is not type(DOMAINS[key][0]): raise ValueError(f'invalid {key}={value!r}; domain: {DOMAINS.get(key, "unknown parameter")}') lim = dict(LIMITS) if limits: if set(limits) - set(LIMITS): raise ValueError('unknown resource limit') lim.update(limits) for key in ('max_axis', 'max_height', 'max_volume'): if type(lim[key]) is not int or lim[key] <= 0: raise ValueError(f'{key} must be a positive integer') heights = lim['tree_height'] if not isinstance(heights, (tuple, list)) or len(heights) != 2 or any(type(h) is not int for h in heights) or not 10 <= heights[0] <= heights[1] <= 32: raise ValueError('tree_height must be [minimum, maximum] within 10..32') if lim['floor_height'] != 6: raise ValueError('floor_height is fixed at 6 for the stair components') rng = random.Random(seed) p = {k: rng.choice(v) for k, v in DOMAINS.items()} p.update(overrides) def condition(key, allowed, reason): if p[key] not in allowed: if key in overrides: raise ValueError(f'{key}={p[key]!r}: {reason}; allowed {allowed}') p[key] = rng.choice(tuple(allowed)) condition('wing_floors', range(1, min(2, p['floors']) + 1), 'wings cannot exceed the main full-storey count') if p['floors'] == 1: condition('balcony', (False,), 'balconies require a full upper storey') if p['topology'] in ('twin', 'courtyard'): condition('porch', ('entry', 'full'), 'side wings occupy the wraparound interface') base = {'garden': 4, 'terrace': 7, 'cliff': 16}[p['terrain']] w, d = p['width'], p['depth'] volumes = [Volume('main', 0, 0, w - 1, d - 1, base, p['floors'], p['ridge'], p['profile'], attic=True)] links = [] def add(name, host, side, width, length, alignment, floors, axis, kind='house', profile=None): if side in ('left', 'right'): z0 = alignment x0 = host.x0 - width + 1 if side == 'left' else host.x1 v = Volume(name, x0, z0, x0 + width - 1, z0 + length - 1, base, floors, axis, profile or p['profile'], kind, parent=host.name) z = (max(host.z0, v.z0) + min(host.z1, v.z1)) // 2 x = host.x0 if side == 'left' else host.x1 links.append({'a': host.name, 'b': name, 'x': x, 'z': z, 'axis': 'x', 'floor': base}) else: x0 = alignment v = Volume(name, x0, host.z1, x0 + width - 1, host.z1 + length - 1, base, floors, axis, profile or p['profile'], kind, parent=host.name) x = (max(host.x0, v.x0) + min(host.x1, v.x1)) // 2 links.append({'a': host.name, 'b': name, 'x': x, 'z': host.z1, 'axis': 'z', 'floor': base}) volumes.append(v) return v main = volumes[0] wing_axis = p['ridge'] if p['wing_ridge'] == 'parallel' else ('x' if p['ridge'] == 'z' else 'z') count = {'single': 0, 'ell': 1, 'twin': 2, 'courtyard': 2, 'staggered': 1}[p['topology']] first_side = rng.choice(('left', 'right')) if p['porch'] == 'wrap' and p['conservatory'] and count == 1: first_side = 'right' sides = [first_side] if count == 1 else ['left', 'right'] if count == 2 else [] for i, side in enumerate(sides): width = p['wing_size'] length = rng.choice((11, 13, 15)) z0 = {'front': -2, 'middle': (d - length) // 2, 'rear': d - length + 2}[p['placement']] if p['topology'] == 'courtyard': z0, length = -rng.choice((5, 7)), d - 3 elif i: z0 += rng.choice((-2, 2)) add('wing_' + side, main, side, width, length, z0, p['wing_floors'], wing_axis) rear_host = main if p['topology'] == 'staggered': rear_host = add('rear_wing', main, 'rear', p['wing_size'], rng.choice((9, 11, 13)), (w - p['wing_size']) // 2, p['wing_floors'], 'z') if p['tower'] != 'none': tw = p['tower_width'] add('tower', rear_host, 'rear', tw, tw, (rear_host.x0 + rear_host.x1 - tw + 1) // 2, min(5, p['floors'] + p['tower_extra']), 'z', p['tower'], 'bell' if p['tower'] == 'timber' else 'chalet') if p['conservatory']: host = next((v for v in volumes if v.name == 'wing_right'), main) length = min(11, host.z1 - host.z0 + 1) add('conservatory', host, 'right', rng.choice((7, 9)), length, (host.z0 + host.z1 - length + 1) // 2, 1, 'z', 'glass', 'chalet') free_sides = [side for side in ('left', 'right') if not any(v.name == 'wing_' + side for v in volumes) and not (side == 'right' and p['conservatory'])] if not free_sides: condition('porch', ('entry', 'full'), 'no free side for a wraparound gallery') p['wrap_side'] = free_sides[0] if free_sides and p['porch'] == 'wrap' else None ex = w // 2 + {'center': 0, 'left': -3, 'right': 3}[p['entrance']] porch_width = 7 if p['porch'] == 'entry' else w porch_x0 = max(0, min(ex - porch_width // 2, w - porch_width)) porch = (porch_x0, -4, porch_x0 + porch_width - 1, -1) front = min(v.z0 for v in volumes) - 6 approach = [] rises = base - 1 landing_length = 3 if p['approach'] == 'landing' else 0 startz = front - rises - landing_length - 1 y, z = 1, startz approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'landing'}) for i in range(rises): if i == rises // 2 and landing_length: for _ in range(landing_length): z += 1 approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'landing'}) y, z = y + 1, z + 1 approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'step'}) for zz in range(z + 1, 1): approach.append({'x': ex, 'z': zz, 'y': base, 'kind': 'landing'}) # Dormers attach to the main attic, far enough inside the wall to open into it. for i in range(p['dormers']): side = -1 if i == 0 else 1 vc = main.v0 + 5 if i == 0 else main.v1 - 5 ua, ub = (main.u0 - 1, main.uc) if side < 0 else (main.uc, main.u1 + 1) a, _, b = main.xyz(ua, 0, vc - 2) c, _, e = main.xyz(ub, 0, vc + 2) volumes.append(Volume(f'dormer_{i}', a, b, c, e, main.levels[-1], 1, 'x' if main.axis == 'z' else 'z', 'chalet', 'dormer', parent='main')) xmin = min(v.x0 for v in volumes) - 7 xmax = max(v.x1 for v in volumes) + 7 zmin = startz - 3 zmax = max(v.z1 for v in volumes) + 7 sites = [(xmin + 2, max(1, main.z1 - 2)), (xmax - 2, main.z1 + 2), ((xmin + xmax) // 2 - 6, zmax - 2), (xmin + 3, main.z0 - 1), (xmax - 2, main.z0 + 2)] trees = [(x, z, rng.randint(*lim['tree_height'])) for x, z in sites[:p['trees']]] maxroof = max(v.roof((v.x0 + v.x1) // 2, (v.z0 + v.z1) // 2) for v in volumes) height = max(maxroof + 7, base + lim['tree_height'][1] + 3) origin = (2 - xmin, 0, 2 - zmin) size = (xmax - xmin + 5, height + 1, zmax - zmin + 5) if max(size[0], size[2]) > lim['max_axis'] or size[1] > lim['max_height'] or size[0] * size[1] * size[2] > lim['max_volume']: raise ValueError(f'design bounds {size} exceed resource limits {lim}') return Plan(seed, p, volumes, links, (ex, base + 1, 0), porch, front, approach, trees, origin, size, lim, overrides)
lodge/geometry.py
# Compile the lodge plan into sealed shells, circulation, furnishings, and landscape. from __future__ import annotations import json import math import random from pathlib import Path from functools import lru_cache from .plan import Plan, Volume STATE_FILE = Path('/work/generator/MCRender/resources/1.21.1-blocks.json') STATES = json.loads(STATE_FILE.read_text()) STONE = ('stone_bricks',) * 8 + ('andesite', 'cobblestone', 'mossy_stone_bricks') ROCK = ('stone',) * 9 + ('andesite', 'tuff', 'cobblestone') PAVING = ('stone_bricks',) * 9 + ('andesite', 'mossy_stone_bricks', 'cobblestone') ROOF = ('deepslate_tiles',) * 12 + ('deepslate_bricks', 'cobbled_deepslate') PALETTES = { 'honey': ('smooth_sandstone', 'stripped_spruce_log', 'spruce_planks', 'dark_oak_log', 'yellow_stained_glass', 'orange'), 'ivory': ('calcite', 'stripped_dark_oak_log', 'spruce_planks', 'dark_oak_log', 'white_stained_glass', 'green'), 'ochre': ('smooth_sandstone', 'stripped_oak_log', 'oak_planks', 'spruce_log', 'yellow_stained_glass', 'red'), } DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)} @lru_cache(None) def blockstate(name, properties=()): """Validate and canonicalize a vanilla state with explicit default properties.""" name = name.removeprefix('minecraft:') if name not in STATES: raise ValueError(f'not a Java 1.21.1 block: {name}') domains, defaults = STATES[name] values = dict(defaults) for key, value in properties: if key not in domains or str(value) not in domains[key]: raise ValueError(f'invalid {name}[{key}={value}]') values[key] = str(value) return 'minecraft:' + name + ('[' + ','.join(f'{k}={v}' for k, v in sorted(values.items())) + ']' if values else '') def name_of(state): return (state or 'minecraft:air').split('[')[0].removeprefix('minecraft:') class Scene: """Sparse local-coordinate scene with shared spatial reservations and evidence.""" def __init__(self, plan: Plan): self.plan = plan self.rng = random.Random(plan.seed) self.blocks = {} self.reserved = set() self.walks = set() self.windows = [] self.doors = [] self.beds = [] self.stairs = [] self.ladders = [] self.rooms = [] self.lights = [] self.attachments = [] self.enclosed = [] self.ground = {} self.furniture = [] self.plaster, self.timber, self.floor, self.beam, self.glass, self.color = PALETTES[plan.parameters['palette']] def p(self, x, y, z, name, **props): self.blocks[x, y, z] = blockstate(name, tuple(sorted(props.items()))) def get(self, x, y, z): return name_of(self.blocks.get((x, y, z))) def box(self, bounds, name, **props): 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.p(x, y, z, name, **props) def mix(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.p(x, y, z, self.rng.choice(palette)) def clear(self, bounds): self.box(bounds, 'air') def reserve(self, point, headroom=2): x, y, z = point self.walks.add(point) for dy in range(headroom): self.reserved.add((x, y + dy, z)) def lamp(self, x, y, z, hanging=False): self.p(x, y, z, 'lantern', hanging=str(hanging).lower()) self.lights.append((x, y, z)) self.attachments.append(((x, y, z), (x, y + (1 if hanging else -1), z))) def door(self, x, y, z, facing): for half, dy in (('lower', 0), ('upper', 1)): self.p(x, y + dy, z, 'spruce_door', half=half, facing=facing) if self.get(x, y + 2, z) == 'air': self.p(x, y + 2, z, self.glass) self.doors.append((x, y, z)) dx, dz = DIRECTIONS[facing] for side in (-1, 1): self.reserve((x + side * dx, y, z + side * dz)) def stair(self, x, y, z, facing, material='spruce', walking=False): self.p(x, y, z, material + '_stairs', facing=facing) if walking: self.stairs.append((x, y, z, facing)) self.reserve((x, y + 1, z), 3) def occupied_elsewhere(self, volume, x, y, z): return any(v is not volume and v.interior(x, y, z) for v in self.plan.volumes) def roof_covered(self, volume, x, y, z): return any(v is not volume and v.contains(x, z) and v.base <= y < v.roof(x, z) for v in self.plan.volumes) def route_reservations(self): """Reserve main axes, portals, furniture access, stairs, and ladder landings.""" p = self.plan for v in p.volumes: if v.kind == 'dormer': for vv in range(v.v0 + 1, v.v1): self.reserve(v.xyz(v.uc, v.base + 1, vv), 3) continue for floor in v.levels: for u in range(v.uc - 1, v.uc + 2): for vv in range(v.v0 + 1, v.v1): x, y, z = v.xyz(u, floor + 1, vv) if y + 1 < v.roof(x, z): self.reserve((x, y, z)) # A crossing axis keeps side-wing interfaces connected. vv = (v.v0 + v.v1) // 2 for u in range(v.u0 + 1, v.u1): self.reserve(v.xyz(u, floor + 1, vv)) if v.name == 'main': for floor in v.levels: for u in range(v.u0 + 2, v.u1 - 1): for vv in (v.v0 + 1, v.v0 + 2, v.v1 - 2, v.v1 - 1): self.reserve(v.xyz(u, floor + 1, vv)) sign = -1 if p.parameters['stair_side'] == 'left' else 1 us = (v.uc + sign * 2, v.uc + sign * 3) for floor in v.levels: for vv in range(v.v1 - 9, v.v1): self.reserve(v.xyz(v.uc + sign * 5, floor + 1, vv)) for floor in v.levels[:-1]: for i in range(6): vv = v.v1 - 8 + i for u in us: x, y, z = v.xyz(u, floor + i + 1, vv) for yy in range(floor + 1, floor + 10): self.reserved.add((x, yy, z)) self.reserve((x, y + 1, z), 3) for u in range(min(us) - 1, max(us) + 2): for vv in (v.v1 - 9, v.v1 - 2, v.v1 - 1): self.reserve(v.xyz(u, floor + 1, vv)) self.reserve(v.xyz(u, floor + 7, vv)) elif len(v.levels) > 1: x, _, z = v.xyz(v.uc, 0, v.v1 - 1) for y in range(v.base + 1, v.levels[-1] + 3): self.reserved.add((x, y, z)) px0, pz0, px1, _ = p.porch post_xs = {px0, px1} if px1 - px0 > 9: post_xs.update((p.entrance[0] - 3, p.entrance[0] + 3)) for x in post_xs: for z in (pz0 - 1, pz0): for y in range(p.volumes[0].base + 1, p.volumes[0].base + 5): self.reserved.add((x, y, z)) for v in p.volumes: if v.kind == 'stone': f = v.levels[-1] zz = (v.z0 + v.z1) // 2 for x in range(v.x1 - 1, v.x1 + 3): for z in range(zz - 2, zz + 3): self.reserve((x, f + 1, z), 3) for link in p.links: x, z, f = link['x'], link['z'], link['floor'] for dx in range(-2, 3): for dz in range(-2, 3): self.reserve((x + dx, f + 1, z + dz), 3) ex, ey, ez = p.entrance for x in range(ex - 1, ex + 2): for z in range(ez - 4, ez + 4): self.reserve((x, ey, z), 3) if p.parameters['balcony']: for x in range(ex - 3, ex + 4): for z in range(-3, 4): self.reserve((x, ey + 6, z), 3) def terrain(self): """Grow solid crag columns and an irregular garden island around the plan.""" plan = self.plan base = plan.volumes[0].base ox, _, oz = plan.origin xmin, xmax = 2 - ox, plan.size_xyz[0] - ox - 3 zmin, zmax = 2 - oz, plan.size_xyz[2] - oz - 3 masses = [v for v in plan.volumes if v.kind != 'dormer'] cx = (xmin + xmax) / 2 cz = (zmin + zmax) / 2 for x in range(xmin, xmax + 1): for z in range(zmin, zmax + 1): corner = max(0, xmin + 4 - x, x - xmax + 4) + max(0, zmin + 4 - z, z - zmax + 4) if corner > 4 + self.rng.randrange(2): continue dist = min(max(v.x0 - x, 0, x - v.x1) + max(v.z0 - z, 0, z - v.z1) for v in masses) h = 1 if dist < 6 and z >= plan.terrace_front: h = max(1, base - 1 - max(0, dist - 2) * (3 if base > 10 else 1)) if dist <= 2: h = base if plan.porch[0] - 2 <= x <= plan.porch[2] + 2 and plan.terrace_front <= z < 1: h = base if x in (xmin, xmax) or z in (zmin, zmax): h = 1 seam = self.rng.choice(('stone', 'stone', 'andesite', 'tuff')) for y in range(h): self.p(x, y, z, self.rng.choice((seam,) * 15 + ROCK) if base > 10 or y < h - 2 else 'dirt') self.p(x, h, z, self.rng.choice(('grass_block',) * 6 + ('moss_block', 'coarse_dirt'))) self.ground[x, z] = h # Foundations, walks, retaining edges: all connected to terrain at y=0. for v in masses: self.mix((v.x0, 0, v.z0, v.x1, base - 1, v.z1), STONE) for x in range(plan.porch[0] - 1, plan.porch[2] + 2): for z in range(plan.terrace_front, 0): self.mix((x, 0, z, x, base, z), STONE) self.p(x, base, z, self.rng.choice(PAVING)) self.ground[x, z] = base for x, z in list(self.ground): h = self.ground[x, z] if h == base and not any(v.contains(x, z) for v in masses): self.p(x, h, z, self.rng.choice(PAVING)) edge = any(self.ground.get((x + dx, z + dz), -1) < base - 1 for dx, dz in DIRECTIONS.values()) if edge and abs(x - plan.entrance[0]) > 3: self.p(x, h + 1, z, 'cobblestone_wall') if (x + z) % 6 == 0: self.p(x, h + 1, z, 'stone_bricks') self.p(x, h + 2, z, 'stone_brick_slab') for patch in plan.approach: x, z, y = patch['x'], patch['z'], patch['y'] for xx in range(x - 2, x + 3): self.mix((xx, 0, z, xx, y - 1, z), STONE) self.clear((xx, y, z, xx, max(base + 2, y + 3), z)) if patch['kind'] == 'step': self.stair(xx, y, z, 'south', 'stone_brick', True) else: self.p(xx, y, z, self.rng.choice(PAVING)) self.reserve((xx, y + 1, z), 3) self.ground[xx, z] = y if z < plan.terrace_front: for xx in (x - 3, x + 3): self.mix((xx, 0, z, xx, y, z), STONE) self.p(xx, y + 1, z, 'stone_brick_slab') if (z - plan.approach[0]['z']) % 5 == 0: self.p(xx, y + 1, z, 'stone_bricks') self.lamp(xx, y + 2, z) def shells(self): """Union room envelopes before placing roof planes and real timber frames.""" for v in self.plan.volumes: for f in v.levels: self.box((v.x0, f, v.z0, v.x1, f, v.z1), self.floor if v.kind != 'glass' else 'polished_andesite') for x in range(v.x0, v.x1 + 1): for z in range(v.z0, v.z1 + 1): if x not in (v.x0, v.x1) and z not in (v.z0, v.z1): continue u, vv = v.uv(x, z) top = v.roof(x, z) for y in range(v.base + 1, top): if self.occupied_elsewhere(v, x, y, z): continue frame = x in (v.x0, v.x1) and z in (v.z0, v.z1) frame |= ((x - v.x0) % 4 == 0 and z in (v.z0, v.z1)) or ((z - v.z0) % 4 == 0 and x in (v.x0, v.x1)) belt = y in v.levels or y == v.levels[-1] + (1 if v.attic else 4) name = self.timber if frame else self.beam if belt else 'glass' if v.kind == 'glass' else self.rng.choice(STONE) if v.kind == 'stone' else self.plaster self.p(x, y, z, name, **({'axis': 'y' if frame else 'x' if z in (v.z0, v.z1) else 'z'} if name.endswith('log') else {})) if v.kind not in ('glass', 'stone', 'timber'): # Timber follows both gable verges below the slate. if vv in (v.v0, v.v1) and not self.occupied_elsewhere(v, x, top - 1, z): self.p(x, top - 1, z, self.timber, axis=v.axis) for f in v.levels: for x in range(v.x0, v.x1 + 1): for z in (v.z0, v.z1): if not self.occupied_elsewhere(v, x, f, z): self.p(x, f, z, self.beam, axis='x') for z in range(v.z0, v.z1 + 1): for x in (v.x0, v.x1): if not self.occupied_elsewhere(v, x, f, z): self.p(x, f, z, self.beam, axis='z') for v in self.plan.volumes: overhang = 1 if v.kind == 'glass' else 2 for x in range(v.x0 - overhang, v.x1 + overhang + 1): for z in range(v.z0 - overhang, v.z1 + overhang + 1): h = v.roof(x, z) u, vv = v.uv(x, z) toward = 'east' if u < v.uc else 'west' if v.axis == 'x': toward = 'south' if u < v.uc else 'north' if v.kind in ('stone', 'timber', 'glass'): ds = [(x - v.x0, 'east'), (v.x1 - x, 'west'), (z - v.z0, 'south'), (v.z1 - z, 'north')] toward = min(ds)[1] dx, dz = DIRECTIONS[toward] low = min(h, v.roof(x - dx, z - dz)) for y in range(low, h + 1): if not self.roof_covered(v, x, y, z): self.p(x, y, z, 'glass' if v.kind == 'glass' else self.rng.choice(ROOF)) if v.kind != 'glass' and not self.roof_covered(v, x, h + 1, z): if u == v.uc and v.kind not in ('stone', 'timber'): self.p(x, h + 1, z, 'deepslate_tile_slab') else: edge = x in (v.x0 - overhang, v.x1 + overhang) or z in (v.z0 - overhang, v.z1 + overhang) self.stair(x, h + 1, z, toward, 'cobbled_deepslate' if edge else 'deepslate_tile') # Carved ridge ends, anchored into the roof cap. if v.kind not in ('glass', 'stone', 'timber'): for vv in (v.v0 - 2, v.v1 + 2): x, _, z = v.xyz(v.uc, 0, vv) h = v.roof(x, z) if not self.roof_covered(v, x, h + 2, z): self.p(x, h + 1, z, self.timber, axis=v.axis) self.p(x, h + 2, z, 'oak_fence') if v.name == 'main' and self.plan.parameters['profile'] != 'chalet': self.p(x, h + 3, z, 'lightning_rod') def opening(self, x, f, z, face, width=2): """Glaze an exterior bay only when it has clear indoor and outdoor space.""" dx, dz = DIRECTIONS[face] cells = [(x + (i if dz else 0), y, z + (i if dx else 0)) for i in range(width) for y in (f + 2, f + 3)] inside = [(a - dx, b, c - dz) for a, b, c in cells] outside = [(a + dx, b, c + dz) for a, b, c in cells] if any(c in self.reserved for c in cells): return if any(any(v.interior(*q) for v in self.plan.volumes) for q in outside): return if any(self.get(*q) not in ('air',) for q in inside + outside): return if any(self.get(*q) in ('air', 'glass') for q in cells): return for q in cells: self.p(*q, self.glass) self.windows.append({'cells': cells, 'inside': inside, 'outside': outside}) self.reserved.update(inside + outside + cells) for i in range(width): xx, zz = x + (i if dz else 0), z + (i if dx else 0) self.p(xx + dx, f + 1, zz + dz, 'spruce_trapdoor', facing=face, half='top') self.attachments.append(((xx + dx, f + 1, zz + dz), (xx, f + 1, zz))) # Compact shutters anchor to wall blocks, never cover the glazing. for i in (-1, width): xx, zz = x + (i if dz else 0), z + (i if dx else 0) for y in (f + 2, f + 3): q = (xx + dx, y, zz + dz) if self.get(xx, y, zz) != 'air' and self.get(*q) == 'air' and q not in self.reserved: self.p(*q, 'spruce_trapdoor', facing=face, open='true') self.attachments.append((q, (xx, y, zz))) def gable_details(self): """Frame vaulted attic gables with tall paired lights and collar beams.""" v = self.plan.volumes[0] f = v.levels[-1] for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')): dx, dz = DIRECTIONS[face] peak = v.roof(*v.xyz(v.uc, 0, vv)[::2]) for y in (f + 6, f + 12): for u in range(v.u0 + 2, v.u1 - 1): x, _, z = v.xyz(u, 0, vv) if y < v.roof(x, z) - 2 and not self.occupied_elsewhere(v, x, y, z): self.p(x, y, z, self.beam, axis='x' if v.axis == 'z' else 'z') for u in (v.uc - 1, v.uc + 1): x, _, z = v.xyz(u, 0, vv) top = min(f + 9, v.roof(x, z) - 3) cells = [(x, y, z) for y in range(f + 3, top + 1)] inside = [(x - dx, y, z - dz) for _, y, _ in cells] outside = [(x + dx, y, z + dz) for _, y, _ in cells] if cells and all(q not in self.reserved and self.get(*q) == 'air' for q in inside + outside) and not any(any(other.interior(*q) for other in self.plan.volumes) for q in outside): for q in cells: self.p(*q, self.glass) self.windows.append({'cells': cells, 'inside': inside, 'outside': outside}) self.reserved.update(cells + inside + outside) x, _, z = v.xyz(v.uc, 0, vv) for y in range(f + 2, peak): if not self.occupied_elsewhere(v, x, y, z): self.p(x, y, z, self.timber, axis='y') # Corbels and small low lamps make the timber frame read in relief. for v in self.plan.volumes: if v.kind in ('glass', 'dormer'): continue for f in v.levels[1:]: for x in range(v.x0, v.x1 + 1, 4): for z, face in ((v.z0 - 1, 'south'), (v.z1 + 1, 'north')): dx, dz = DIRECTIONS[face] if self.get(x + dx, f - 1, z + dz) != 'air': self.exterior_p(x, f - 1, z, 'spruce_stairs', facing=face, half='top') for x in (v.x0, v.x1): z = v.z0 - 1 y = v.base + 3 if all(self.get(x, yy, z) == 'air' and (x, yy, z) not in self.reserved for yy in (y, y + 1)) and not any(other.contains(x, z) for other in self.plan.volumes): self.p(x, y + 1, z, self.beam, axis='z') self.lamp(x, y, z, True) def glazing(self): for v in self.plan.volumes: if v.kind == 'glass': continue for f in v.levels: for face, z in (('north', v.z0), ('south', v.z1)): for x in range(v.x0 + 2, v.x1 - 1, 4): self.opening(x, f, z, face) for face, x in (('west', v.x0), ('east', v.x1)): for z in range(v.z0 + 2, v.z1 - 1, 4): self.opening(x, f, z, face) if v.attic: for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')): x, f, z = v.xyz(v.uc - 1, v.levels[-1], vv) self.opening(x, f + 1, z, face, 3) def openings(self): for link in self.plan.links: x, z, f = link['x'], link['z'], link['floor'] if link['axis'] == 'x': self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1)) self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor) else: self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1)) self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor) x, y, z = self.plan.entrance self.clear((x, y, z - 1, x, y + 2, z + 1)) self.door(x, y, z, 'north') for xx in (x - 1, x + 1): self.box((xx, y, z - 1, xx, y + 2, z - 1), self.timber, axis='y') self.box((x - 1, y + 3, z - 1, x + 1, y + 3, z - 1), self.beam, axis='x') def circulation(self): """Cut stacked stairs and supported ladders after all intersecting roofs.""" for v in self.plan.volumes: if v.name == 'main': sign = -1 if self.plan.parameters['stair_side'] == 'left' else 1 for f in v.levels[:-1]: for i in range(6): for u in (v.uc + 2 * sign, v.uc + 3 * sign): x, y, z = v.xyz(u, f + i + 1, v.v1 - 8 + i) self.clear((x, y + 1, z, x, y + 3, z)) self.p(x, y - 1, z, self.floor) self.stair(x, y, z, 'south' if v.axis == 'z' else 'east', walking=True) for u in (v.uc + 2 * sign, v.uc + 3 * sign): for vv in (v.v1 - 2, v.v1 - 1): x, _, z = v.xyz(u, 0, vv) self.p(x, f + 6, z, self.floor) self.clear((x, f + 7, z, x, f + 9, z)) # Outer guard on each stair opening, leaving return aisles clear. for f in v.levels[1:]: for vv in range(v.v1 - 7, v.v1 - 2): u = v.uc + 4 * sign x, y, z = v.xyz(u, f + 1, vv) if self.get(x, f, z) != 'air' and (x, y, z) not in self.reserved and y + 2 < v.roof(x, z): self.p(x, y, z, 'spruce_fence') elif len(v.levels) > 1: x, _, z = v.xyz(v.uc, 0, v.v1 - 1) bx, _, bz = v.xyz(v.uc, 0, v.v1) self.box((bx, v.base + 1, bz, bx, v.levels[-1] + 3, bz), self.beam, axis='y') for y in range(v.base + 1, v.levels[-1] + 3): self.p(x, y, z, 'ladder', facing='north' if v.axis == 'z' else 'west') self.ladders.append((x, y, z)) for f in v.levels: u = v.uc - 1 xx, yy, zz = v.xyz(u, f + 1, v.v1 - 1) self.clear((xx, yy, zz, xx, yy + 1, zz)) self.reserve((xx, yy, zz)) def room(self, v, f, purpose, bounds, target): self.rooms.append({'volume': v.name, 'floor': f, 'purpose': purpose, 'bounds': bounds, 'target': target, 'furniture': [], 'lights': []}) self.reserve(target) return self.rooms[-1] def furnish_cell(self, room, x, y, z, name, **props): if (x, y, z) in self.reserved or self.get(x, y, z) != 'air' or self.get(x, y - 1, z) == 'air': return False self.p(x, y, z, name, **props) room['furniture'].append((x, y, z)) self.furniture.append((x, y, z)) return True def bed(self, room, x, y, z, facing): dx, dz = DIRECTIONS[facing] pts = [(x, y, z), (x + dx, y, z + dz)] if any(q in self.reserved or self.get(*q) != 'air' or not self.get(q[0], y - 1, q[2]) or self.get(q[0], y - 1, q[2]) == 'air' or self.get(q[0], y + 1, q[2]) != 'air' for q in pts): return False for q, part in zip(pts, ('foot', 'head')): self.p(*q, self.color + '_bed', facing=facing, part=part) room['furniture'].append(q) self.beds.append(pts[0]) for xx, yy, zz in pts: for a, b in DIRECTIONS.values(): if all(self.get(xx + a, yy + dy, zz + b) == 'air' for dy in (0, 1)): self.reserve((xx + a, yy, zz + b)) return True def interiors(self): """Allocate functions to actual rooms, with layouts responding to the plan.""" for v in self.plan.volumes: if v.kind == 'dormer': # Its floor continues the furnished attic, with a useful window bench. continue for level, f in enumerate(v.levels): layout = self.plan.parameters['layout'] vc = (v.v0 + v.v1) // 2 + (2 if layout == 'suites' else 0) segments = [(v.v0 + 1, v.v1 - 1)] if v.name == 'main' and level < v.floors and layout != 'open': segments = [(v.v0 + 1, vc - 1), (vc + 1, v.v1 - 1)] for u in range(v.u0 + 1, v.u1): for y in range(f + 1, f + 6): x, _, z = v.xyz(u, y, vc) if abs(u - v.uc) <= 1 or (x, y, z) in self.reserved: continue self.p(x, y, z, self.timber if u % 4 == 0 else self.plaster, **({'axis': 'y'} if u % 4 == 0 else {})) # A framed three-wide arch leaves the central hall continuous. for u in range(v.uc - 1, v.uc + 2): x, _, z = v.xyz(u, 0, vc) self.p(x, f + 4, z, self.beam, axis='x' if v.axis == 'z' else 'z') if layout == 'suites' and level > 0: for u in (v.uc - 1, v.uc + 1): for yy in range(f + 1, f + 6): self.p(*v.xyz(u, yy, vc), self.timber, axis='y') self.door(*v.xyz(v.uc, f + 1, vc), 'south' if v.axis == 'z' else 'east') for index, (va, vb) in enumerate(segments): if v.kind == 'glass': purpose = 'winter garden and herbal workroom' elif v.name == 'main': purpose = ('hearth kitchen and dining', 'parlour and pantry')[index] if level == 0 else 'bedchamber and study' if level == len(v.levels) - 1 else ('guest suite' if layout == 'suites' else 'library and workroom') elif v.name == 'tower': purpose = ('pantry', 'map room', 'watchkeeper bedroom', 'archive', 'lookout study')[min(level, 4)] else: purpose = 'guest bedroom' if level else self.rng.choice(('craft workshop', 'dining parlour', 'library')) ua, ub = v.u0 + 1, v.u1 - 1 target = v.xyz(v.uc, f + 1, (va + vb) // 2) x0, _, z0 = v.xyz(ua, 0, va) x1, _, z1 = v.xyz(ub, 0, vb) room = self.room(v, f, purpose, (x0, f + 1, z0, x1, f + 4, z1), target) # Furnishing banks on both sides of the protected longitudinal hall. spots = [] for u in (ua + 1, ub - 1): for vv in range(va + 1, vb, 2): x, y, z = v.xyz(u, f + 1, vv) if y + 3 < v.roof(x, z) and all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)): spots.append((x, y, z)) # The minimum room width always admits perimeter furniture, even at ladders. for x in range(x0, x1 + 1): for z in range(z0, z1 + 1): y = f + 1 if all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)) and self.get(x, f, z) != 'air': spots.append((x, y, z)) # Bound furniture density and retain walkable gaps, including low attics. spaced = [] for q in spots: if all(abs(q[0] - r[0]) + abs(q[2] - r[2]) >= 3 for r in spaced): spaced.append(q) spots = spaced[:max(4, min(12, ((ub - ua + 1) * (vb - va + 1)) // 16))] if not spots: raise AssertionError(f'no furnishing positions in {v.name} {f}') # Every room has a low supported lamp and a second task light when large. for lamp_index in sorted(set((0, len(spots) - 1))): x, y, z = spots[lamp_index] self.p(x, y, z, 'barrel', facing='up') room['furniture'].append((x, y, z)) self.lamp(x, y + 1, z) room['lights'].append((x, y + 1, z)) candidates = spots[1:-1] or spots[1:] wants_bed = 'bed' in purpose or 'suite' in purpose if wants_bed: found = False for x, y, z in candidates: if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'): found = True break if not found: for u in range(ua, ub + 1): for vv in range(va, vb): x, y, z = v.xyz(u, f + 1, vv) if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'): found = True break if found: break assert found, f'bed has no valid location in {v.name} {f}' furnishings = ('smoker', 'crafting_table', 'barrel', 'furnace', 'bookshelf') if 'kitchen' in purpose else ('composter', 'moss_block', 'barrel', 'crafting_table') if v.kind == 'glass' else ('bookshelf', 'cartography_table', 'barrel', 'loom', 'chest') for i, (x, y, z) in enumerate(candidates): name = furnishings[i % len(furnishings)] props = {'facing': 'south'} if name in ('smoker', 'furnace', 'chest', 'loom', 'barrel') else {} if name in ('smoker', 'furnace'): props['lit'] = 'true' if self.furnish_cell(room, x, y, z, name, **props): if i % 2 == 0 and name not in ('composter', 'chest') and (x, y + 1, z) not in self.reserved: if name == 'bookshelf' and self.get(x, y + 2, z) == 'air': self.p(x, y + 1, z, 'bookshelf') else: self.p(x, y + 1, z, 'potted_fern' if v.kind != 'glass' else self.rng.choice(('potted_azalea_bush', 'potted_poppy', 'potted_dandelion'))) self.attachments.append(((x, y + 1, z), (x, y, z))) # A table and facing seat fill a front-side bay without covering the hall. for offset in (() if wants_bed else (-3, 3)): x, y, z = v.xyz(v.uc + offset, f + 1, va + 1) if self.furnish_cell(room, x, y, z, self.floor): if (x, y + 1, z) not in self.reserved and self.get(x, y + 1, z) == 'air': self.p(x, y + 1, z, 'potted_spruce_sapling') self.attachments.append(((x, y + 1, z), (x, y, z))) sx, sy, sz = v.xyz(v.uc + offset, f + 1, va + 2) self.furnish_cell(room, sx, sy, sz, 'spruce_stairs', facing='north' if v.axis == 'z' else 'west') # Wool inlays provide colour without reducing headroom. for u in range(v.uc - 1, v.uc + 2): for vv in range(va + 2, min(vb, va + 5)): x, _, z = v.xyz(u, 0, vv) if self.get(x, f, z).endswith('planks'): self.p(x, f, z, self.color + '_wool') self.enclosed.append(target) def dormer_nooks(self): """Furnish and verify the accessible reading bays extending the attic.""" main = self.plan.volumes[0] for v in self.plan.volumes: if v.kind != 'dormer': continue exterior_v = v.v0 + 1 if v.v0 < main.uc else v.v1 - 1 target = v.xyz(v.uc, v.base + 1, exterior_v) room = self.room(v, v.base, 'dormer reading nook', (v.x0 + 1, v.base + 1, v.z0 + 1, v.x1 - 1, v.base + 4, v.z1 - 1), target) # The parent attic remains open behind the bay. Its own small lamp is near the floor. for vv in range(v.v0 + 1, v.v1): for u in (v.uc - 1, v.uc + 1): x, y, z = v.xyz(u, v.base + 1, vv) if all(self.get(x, y + dy, z) == 'air' and (x, y + dy, z) not in self.reserved for dy in (0, 1)): self.p(x, y, z, 'bookshelf') self.lamp(x, y + 1, z) room['furniture'].append((x, y, z)) room['lights'].append((x, y + 1, z)) break if room['lights']: break assert room['lights'], f'no task-light position for {v.name}' self.enclosed.append(target) def chimney(self): """Run a masonry chimney from a supported hearth through the roof shoulder.""" v = self.plan.volumes[0] x, z, f = v.x0 + 1, v.z1 - 2, v.base h = max(v.roof(x, z), v.roof(x + 1, z)) + 5 self.mix((x, f + 1, z, x + 1, h, z + 1), STONE) for yy in (h - 3, h): self.box((x - 1, yy, z - 1, x + 2, yy, z + 2), 'stone_bricks') self.p(x, h + 1, z, 'campfire', lit='true') for xx, zz in ((x - 1, z - 1), (x + 2, z - 1), (x - 1, z + 2), (x + 2, z + 2)): self.p(xx, h + 1, zz, 'stone_brick_wall') self.box((x - 1, h + 2, z - 1, x + 2, h + 2, z + 2), 'stone_bricks') # A contained hearth opens to the room, with a proper masonry hood. for xx in (x, x + 1): self.p(xx, f + 1, z, 'campfire', lit='true') self.p(xx, f + 2, z, 'air') self.p(xx, f + 1, z - 1, 'iron_bars', east='true', west='true') self.p(xx, f + 2, z - 1, 'iron_bars', east='true', west='true') self.box((x, f + 3, z - 1, x + 1, f + 3, z - 1), 'stone_bricks') def exterior_p(self, x, y, z, name, **props): if any(v.contains(x, z) and v.base < y <= v.roof(x, z) + 1 for v in self.plan.volumes): return if (x, y, z) in self.reserved or self.get(x, y, z) != 'air': return self.p(x, y, z, name, **props) def porches(self): """Build post-supported canopies, optional balcony, and an open side gallery.""" p = self.plan f = p.volumes[0].base ex = p.entrance[0] x0, z0, x1, z1 = p.porch self.box((x0, f, z0, x1, f, z1), self.floor) for x in range(x0 - 1, x1 + 2): for z in range(z0 - 1, 0): balcony = p.parameters['balcony'] and ex - 3 <= x <= ex + 3 and -3 <= z <= -1 if not balcony: h = f + 5 + (z - z0) // 2 self.exterior_p(x, h, z, self.rng.choice(ROOF)) self.exterior_p(x, h + 1, z, 'deepslate_tile_slab') posts = {x0, x1} if x1 - x0 > 9: posts.update((ex - 3, ex + 3)) for x in sorted(posts): if abs(x - ex) <= 1 or any(v.contains(x, z0) for v in p.volumes): continue self.mix((x, 0, z0, x, f, z0), STONE) self.box((x, f + 1, z0, x, f + 4, z0), self.timber, axis='y') self.p(x, f + 4, z0 - 1, self.floor) self.lamp(x, f + 3, z0 - 1, True) for dx in (-1, 1): if x0 <= x + dx <= x1: self.p(x + dx, f + 4, z0, 'spruce_stairs', facing='east' if dx < 0 else 'west', half='top') for x in range(x0, x1 + 1): self.exterior_p(x, f + 4, z0, self.beam, axis='x') for x in range(x0 + 1, x1): if abs(x - ex) > 2: self.p(x, f + 1, z0, 'spruce_fence') if p.parameters['balcony']: self.box((ex - 3, f + 6, -3, ex + 3, f + 6, -1), self.floor) for x in range(ex - 3, ex + 4): self.p(x, f + 7, -3, 'spruce_fence') for x in (ex - 3, ex + 3): self.box((x, f + 1, -3, x, f + 6, -3), self.timber, axis='y') for z in (-2, -1): self.p(x, f + 7, z, 'spruce_fence') self.clear((ex, f + 7, -2, ex, f + 9, 1)) self.door(ex, f + 7, 0, 'north') self.rooms.append({'volume': 'balcony', 'floor': f + 6, 'purpose': 'open balcony', 'bounds': (ex - 2, f + 7, -2, ex + 2, f + 9, -1), 'target': (ex, f + 7, -2), 'furniture': [], 'lights': []}) self.p(ex - 2, f + 7, -1, 'barrel', facing='up') self.lamp(ex - 2, f + 8, -1) self.rooms[-1]['furniture'].append((ex - 2, f + 7, -1)) self.rooms[-1]['lights'].append((ex - 2, f + 8, -1)) if p.parameters['porch'] == 'wrap': main = p.volumes[0] # Choose a free facade interface at planning time. side = p.parameters['wrap_side'] xa, xb = (-4, -1) if side == 'left' else (main.x1 + 1, main.x1 + 4) for x in range(xa, xb + 1): for z in range(-3, main.z1 + 1): self.mix((x, 0, z, x, f - 1, z), STONE) self.p(x, f, z, self.floor) h = f + 5 + (x - xa) // 2 if side == 'left' else f + 5 + (xb - x) // 2 self.exterior_p(x, h, z, self.rng.choice(ROOF)) self.exterior_p(x, h + 1, z, 'deepslate_tile_slab') outer = xa if side == 'left' else xb for z in range(-2, main.z1 + 1, 5): self.box((outer, f + 1, z, outer, f + 4, z), self.timber, axis='y') self.p(outer, f + 5, z, self.beam, axis='z') self.lamp(outer, f + 3, z + 1, True) self.p(outer, f + 4, z + 1, self.floor) for z in range(0, main.z1): if self.get(outer, f + 1, z) == 'air': self.p(outer, f + 1, z, 'spruce_fence') # Stone tower lookout: a glazed room with an east balcony and railed ledges. for v in p.volumes: if v.kind != 'stone': continue top = v.levels[-1] for x in range(v.x0 - 1, v.x1 + 2): for z in range(v.z0 - 1, v.z1 + 2): if v.contains(x, z) or any(other is not v and other.contains(x, z, 2) and top <= other.roof(x, z) + 2 for other in p.volumes): continue self.p(x, top, z, self.floor) self.p(x, top + 1, z, 'oak_fence') if x in (v.x0 - 1, v.x1 + 1) and z in (v.z0, v.z1): self.p(x, top - 1, z, 'spruce_stairs', facing='east' if x < v.x0 else 'west', half='top') # East gallery has a two-wide deck so the rail does not block the door. zz = (v.z0 + v.z1) // 2 for z in range(zz - 2, zz + 3): self.p(v.x1 + 2, top, z, self.floor) self.p(v.x1 + 2, top + 1, z, 'oak_fence') self.p(v.x1 + 1, top + 1, z, 'air') self.door(v.x1, top + 1, zz, 'east') self.reserve((v.x1 - 1, top + 1, zz)) def tree(self, x, z, height): base = self.ground[x, z] self.box((x, base + 1, z, x, base + height - 2, z), 'spruce_log', axis='y') for rise in range(4, height, 3): radius = max(1, min(3, (height - rise + 3) // 5)) for dx, dz in DIRECTIONS.values(): for i in range(1, radius): xx, yy, zz = x + dx * i, base + rise, z + dz * i if self.get(xx, yy, zz) == 'air': self.p(xx, yy, zz, 'spruce_log', axis='x' if dx else 'z') for dy in (0, 1): r = max(1, radius - dy) for dx in range(-r, r + 1): for dz in range(-r, r + 1): if abs(dx) + abs(dz) > r + 1 or (abs(dx) == abs(dz) == r and r > 1): continue xx, yy, zz = x + dx, base + rise + dy, z + dz if self.get(xx, yy, zz) == 'air' and (xx, yy, zz) not in self.reserved: self.p(xx, yy, zz, 'spruce_leaves', persistent='true') for yy in range(base + height - 1, base + height + 2): self.p(x, yy, z, 'spruce_leaves', persistent='true') def landscape(self): """Plant grounded conifers and garden borders while preserving every opening.""" for x, z, height in self.plan.tree_sites: self.tree(x, z, height) for (x, z), y in sorted(self.ground.items()): if self.get(x, y, z) not in ('grass_block', 'moss_block', 'coarse_dirt') or self.get(x, y + 1, z) != 'air' or (x, y + 1, z) in self.reserved: continue n = self.rng.random() if n < .14: self.p(x, y + 1, z, self.rng.choice(('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet'))) self.attachments.append(((x, y + 1, z), (x, y, z))) elif n < .20: self.p(x, y + 1, z, self.rng.choice(('azalea_leaves', 'flowering_azalea_leaves')), persistent='true') elif n < .23: self.p(x, y + 1, z, 'mossy_cobblestone') # Vertical ivy grows only against an actual solid corner post. for v in self.plan.volumes: if v.kind in ('dormer', 'glass'): continue for x, z, face in ((v.x0, v.z0 - 1, 'south'), (v.x1 + 1, v.z1, 'west')): dx, dz = DIRECTIONS[face] for y in range(v.base + 1, min(v.levels[-1] + 4, v.base + 13)): if self.get(x, y, z) == 'air' and (x, y, z) not in self.reserved and self.get(x + dx, y, z + dz).endswith('log'): self.p(x, y, z, 'vine', **{face: 'true'}) self.attachments.append(((x, y, z), (x + dx, y, z + dz))) # Entrance heraldry has physical textile blocks, visible without block entity NBT. if self.plan.parameters['floors'] >= 2: v = self.plan.volumes[0] for x in (v.x0 + 1, v.x1 - 1): for y in range(v.base + 8, v.base + 12): z = -1 if (x, y, z) not in self.reserved and self.get(x, y, z) == 'air' and self.get(x, y, 0) != 'air': self.p(x, y, z, self.color + '_wool') self.p(x, v.base + 12, -1, self.beam, axis='z') def connections(self): """Resolve fence arms from final neighboring geometry.""" for (x, y, z), state in list(self.blocks.items()): name = name_of(state) if name.endswith('_fence'): props = {} for face, (dx, dz) in DIRECTIONS.items(): neighbor = self.get(x + dx, y, z + dz) props[face] = 'true' if neighbor.endswith(('_fence', '_log', '_planks')) or neighbor in ('stone_bricks', 'cobblestone') else 'false' self.p(x, y, z, name, **props) def build(self): self.route_reservations() v = self.plan.volumes[0] for x in range(v.x0, v.x0 + 4): for z in range(v.z1 - 3, v.z1 + 2): for y in range(v.base + 1, self.plan.size_xyz[1]): self.reserved.add((x, y, z)) self.terrain() self.shells() self.openings() self.circulation() self.chimney() self.porches() self.gable_details() self.glazing() self.interiors() self.dormer_nooks() self.landscape() self.connections() return self
lodge/validation.py
# Validate actual final block geometry, physical interfaces, and traversable interiors. from __future__ import annotations from collections import deque from functools import lru_cache import hashlib import json from .geometry import STATES, DIRECTIONS, name_of @lru_cache(None) def parse(state): name = name_of(state) props = {} if state and '[' in state: props = dict(item.split('=') for item in state.split('[', 1)[1][:-1].split(',')) return name, props def walk_passable(name): return name in ('air', 'ladder', 'vine') or name.endswith('_door') def solid_support(name): return name != 'air' and name not in ('ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass') and not name.endswith(('_fence', '_wall', '_door', '_trapdoor', '_slab', '_leaves', '_bed')) and not name.startswith('potted_') def reachable(scene): """Flood walkable standing cells using stair orientation and supported ladders. Args: scene (Scene): Final geometry after every decorative pass. Returns: set: Reachable feet coordinates. Doors are player-operable, closed by default. """ blocks = scene.blocks def at(x, y, z): return name_of(blocks.get((x, y, z))) candidates = set() for (x, y, z), state in blocks.items(): n = name_of(state) if solid_support(n) or n.endswith('_stairs'): candidates.add((x, y + 1, z)) if n == 'ladder': candidates.add((x, y, z)) candidates.add((x, y + 1, z)) nodes = {p for p in candidates if walk_passable(at(*p)) and walk_passable(at(p[0], p[1] + 1, p[2]))} start = scene.plan.approach[0] first = (start['x'], start['y'] + 1, start['z']) assert first in nodes, f'exterior start obstructed {first}: {at(*first)}' seen = {first} queue = deque([first]) while queue: x, y, z = queue.popleft() for face, (dx, dz) in DIRECTIONS.items(): for dy in (0, 1, -1): q = (x + dx, y + dy, z + dz) if q not in nodes or q in seen: continue if dy: stairp = (q[0], q[1] - 1, q[2]) if dy > 0 else (x, y - 1, z) n, props = parse(blocks.get(stairp)) required = face if dy > 0 else next(k for k, d in DIRECTIONS.items() if d == (-dx, -dz)) if not n.endswith('_stairs') or props.get('facing') != required or props.get('half') != 'bottom': continue seen.add(q) queue.append(q) for dy in (-1, 1): q = (x, y + dy, z) if q in nodes and q not in seen and (at(x, y, z) == 'ladder' or at(*q) == 'ladder'): seen.add(q) queue.append(q) return seen def envelope_leaks(scene): """Flood exterior air through porous blocks and detect exposed room targets.""" # Occupied room components share a closed envelope; report the first leaking target. ox, _, oz = scene.plan.origin sx, sy, sz = scene.plan.size_xyz xmin, xmax, zmin, zmax = -ox, sx - ox - 1, -oz, sz - oz - 1 porous = {'air', 'ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass', 'flower_pot', 'lightning_rod'} barriers = set() for p, state in scene.blocks.items(): n = name_of(state) if n in porous or n.endswith(('_stairs', '_slab', '_fence', '_wall', '_leaves', '_bed')) or n.startswith('potted_'): continue barriers.add(p) # Flood from every boundary face, with a compact flat integer visited set. plane = sx * sz def idx(x, y, z): return y * plane + (z + oz) * sx + x + ox blocked = bytearray(sx * sy * sz) for x, y, z in barriers: if xmin <= x <= xmax and zmin <= z <= zmax and 0 <= y < sy: blocked[idx(x, y, z)] = 1 seen = bytearray(len(blocked)) queue = deque() def add(i): if not blocked[i] and not seen[i]: seen[i] = 1 queue.append(i) for y in range(sy): for z in range(sz): for x in (0, sx - 1): add(y * plane + z * sx + x) for x in range(sx): for z in (0, sz - 1): add(y * plane + z * sx + x) for z in range(sz): for x in range(sx): add(z * sx + x) add((sy - 1) * plane + z * sx + x) while queue: i = queue.popleft() y, rem = divmod(i, plane) z, x = divmod(rem, sx) if x: add(i - 1) if x < sx - 1: add(i + 1) if z: add(i - sx) if z < sz - 1: add(i + sx) if y: add(i - plane) if y < sy - 1: add(i + plane) return [p for p in scene.enclosed if seen[idx(*p)]] def isolated_floor_cells(scene, visited): """Find usable interior floor cells that final furniture or framing isolated.""" missed = set() for v in scene.plan.volumes: for f in v.levels: for x in range(v.x0 + 1, v.x1): for z in range(v.z0 + 1, v.z1): if solid_support(scene.get(x, f, z)) and all(walk_passable(scene.get(x, f + dy, z)) for dy in (1, 2)) and (x, f + 1, z) not in visited: missed.add((x, f + 1, z)) return missed def geometry_signature(scene): """Hash actual roof occupancy and room partitions, ignoring material noise. Args: scene (Scene): Final placed or reloaded geometry. Returns: str: Observed sample signature, not a count of possible designs. """ main = scene.plan.volumes[0] mask = set() for (x, y, z), state in scene.blocks.items(): if 'deepslate' in name_of(state): mask.add((x - main.x0, y - main.base, z - main.z0, 'roof')) for volume in scene.plan.volumes: for floor in volume.levels: for x in range(volume.x0, volume.x1 + 1): for z in range(volume.z0, volume.z1 + 1): # Floor, wall and void geometry only. Decoration colour is irrelevant. if solid_support(scene.get(x, floor, z)): mask.add((x - main.x0, floor - main.base, z - main.z0, 'floor')) if not walk_passable(scene.get(x, floor + 1, z)): mask.add((x - main.x0, floor + 1 - main.base, z - main.z0, 'occupied')) return hashlib.sha256(json.dumps(sorted(mask), separators=(',', ':')).encode()).hexdigest() def validate_scene(scene, envelope=True): """Check final geometry, without trusting the plan's declared room graph.""" blocks = scene.blocks get = scene.get p = scene.plan ox, oy, oz = p.origin sx, sy, sz = p.size_xyz assert all(0 <= x + ox < sx and 0 <= y + oy < sy and 0 <= z + oz < sz for x, y, z in blocks), 'bounds violation' for state in set(blocks.values()): n, props = parse(state) assert n in STATES, state domains, defaults = STATES[n] assert set(props) == set(defaults), state assert all(value in domains[key] for key, value in props.items()), state doors, beds = 0, 0 for (x, y, z), state in blocks.items(): n, props = parse(state) if n.endswith('_door'): other = (x, y + (1 if props['half'] == 'lower' else -1), z) nn, pp = parse(blocks.get(other)) assert n == nn and pp.get('half') != props['half'] and all(props[k] == pp[k] for k in ('facing', 'hinge', 'open', 'powered')), f'broken door {(x,y,z)}' if props['half'] == 'lower': assert solid_support(get(x, y - 1, z)), f'unsupported door {(x,y,z)}' doors += 1 if n.endswith('_bed'): dx, dz = DIRECTIONS[props['facing']] sign = 1 if props['part'] == 'foot' else -1 nn, pp = parse(blocks.get((x + sign * dx, y, z + sign * dz))) assert nn == n and pp.get('part') != props['part'] and pp.get('facing') == props['facing'], f'broken bed {(x,y,z)}' assert solid_support(get(x, y - 1, z)), f'unsupported bed {(x,y,z)}' beds += props['part'] == 'foot' if n == 'ladder': dx, dz = DIRECTIONS[props['facing']] assert solid_support(get(x - dx, y, z - dz)), f'unsupported ladder {(x,y,z)}' if n == 'lantern': dy = 1 if props['hanging'] == 'true' else -1 assert get(x, y + dy, z) not in ('air', 'vine', 'glass'), f'unsupported lantern {(x,y,z)}' if n.startswith('potted_') or n in ('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet'): assert solid_support(get(x, y - 1, z)), f'unsupported plant {(x,y,z)}' if n == 'vine': assert any(props.get(face) == 'true' and solid_support(get(x + dx, y, z + dz)) for face, (dx, dz) in DIRECTIONS.items()), f'unattached vine {(x,y,z)}' for q, support in scene.attachments: assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}' for window in scene.windows: assert all('glass' in get(*q) for q in window['cells']), f'overwritten window {window}' assert all(get(*q) == 'air' for q in window['inside'] + window['outside']), f'blocked window {window}' for x, y, z, facing in scene.stairs: n, props = parse(blocks.get((x, y, z))) assert n.endswith('_stairs') and props['facing'] == facing, f'overwritten tread {(x,y,z)}' assert solid_support(get(x, y - 1, z)), f'unsupported tread {(x,y,z)}' assert all(walk_passable(get(x, y + dy, z)) for dy in (1, 2, 3)), f'stair headroom {(x,y,z)}' visited = reachable(scene) for room in scene.rooms: assert room['target'] in visited, f'unreachable {room["volume"]}/{room["purpose"]}: {room["target"]}' assert room['furniture'], f'unfurnished room {room}' assert room['lights'], f'unlit room {room}' assert all(get(*q) == 'lantern' for q in room['lights']), f'overwritten room lights {room}' assert all(abs(q[1] - room['floor']) <= 3 for q in room['lights']), f'light too high {room}' missed = isolated_floor_cells(scene, visited) assert not missed, f'isolated usable floor cells: {sorted(missed)[:12]}' for x, y, z in scene.doors: n, props = parse(blocks.get((x, y, z))) assert n.endswith('_door'), f'overwritten door {(x, y, z)}' dx, dz = DIRECTIONS[props['facing']] for sign in (-1, 1): assert all(walk_passable(get(x + sign * dx, y + dy, z + sign * dz)) for dy in (0, 1)), f'blocked doorway {(x, y, z)}' for v in p.volumes: if v.kind != 'dormer': assert all(solid_support(get(x, y, z)) for x in range(v.x0, v.x1 + 1) for z in range(v.z0, v.z1 + 1) for y in range(v.base)), f'discontinuous foundation {v.name}' assert beds >= 1, 'no sleeping accommodation' if envelope: leaks = envelope_leaks(scene) assert not leaks, f'weather envelope leaks to {leaks}' return {'rooms': len(scene.rooms), 'beds': beds, 'doors': doors, 'windows': len(scene.windows), 'walking_stairs': len(scene.stairs), 'reachable_cells': len(visited), 'blocks': sum(name_of(s) != 'air' for s in blocks.values()), 'envelope_checked': envelope}
test/test_generator.py
# Exercise real block geometry over a finite corpus, targeted cases, and deliberate defects. from __future__ import annotations import copy import json import subprocess import sys from pathlib import Path import pytest from generate import Build, generate, blockstates_equivalent from blocks import load_schematic from mcio.nbt.nbt import load as load_nbt from lodge.plan import DEFAULT_SEED, DOMAINS, resolve from lodge.geometry import DIRECTIONS, blockstate, name_of from lodge.validation import validate_scene, envelope_leaks, geometry_signature, parse, reachable from cases import TARGETS CORPUS = list(range(100)) + [-(2**63), 2**63 - 1, DEFAULT_SEED, 20260923] @pytest.mark.parametrize('seed', CORPUS) def test_seed_corpus(seed): build = generate(seed) scene = build.scene for room in scene.rooms: x0, y0, z0, x1, y1, z1 = room['bounds'] contents = {name_of(state) for (x, y, z), state in scene.blocks.items() if x0 <= x <= x1 and y0 <= y <= y1 and z0 <= z <= z1} if 'kitchen' in room['purpose']: assert 'smoker' in contents and 'crafting_table' in contents, (seed, room) if 'bed' in room['purpose'] or 'suite' in room['purpose']: assert any(name.endswith('_bed') for name in contents), (seed, room) visited = reachable(scene) for foot in scene.beds: x, y, z = foot _, props = parse(scene.blocks[foot]) dx, dz = DIRECTIONS[props['facing']] assert any((xx + a, y, zz + b) in visited for xx, zz in ((x, z), (x + dx, z + dz)) for a, b in DIRECTIONS.values()), (seed, foot) @pytest.mark.parametrize('name,parameters', TARGETS, ids=[n for n, _ in TARGETS]) def test_targeted_geometry(name, parameters): build = generate(907, parameters) assert build.validation['envelope_checked'] assert all(any(r['volume'] == v.name and r['floor'] == f for r in build.scene.rooms) for v in build.plan.volumes for f in v.levels) @pytest.mark.parametrize('seed', [0, 8, 31, DEFAULT_SEED, -(2**63), 2**63 - 1]) def test_determinism_and_semantic_roundtrip(seed, artifact_dir): a = generate(seed, output=artifact_dir / 'a.litematic') b = generate(seed, output=artifact_dir / 'b.litematic') assert a.placed == b.placed assert (artifact_dir / 'a.litematic').read_bytes() == (artifact_dir / 'b.litematic').read_bytes() assert a.plan.record() == b.plan.record() root = load_nbt(artifact_dir / 'a.litematic') assert len(root['Regions']) == 1 assert int(root['Metadata']['TimeCreated']) == int(root['Metadata']['TimeModified']) == 0 loaded = load_schematic(artifact_dir / 'a.litematic') assert loaded.size_yzx == (a.size_y, a.size_z, a.size_x) ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) ox, oy, oz = a.plan.origin scene = copy.copy(a.scene) scene.blocks = {p: loaded.palette[int(ids[p[1] + oy, p[2] + oz, p[0] + ox])] for p in a.scene.blocks} assert geometry_signature(scene) == geometry_signature(a.scene) validate_scene(scene) record = json.loads((artifact_dir / 'a.json').read_text()) replay = generate(record['seed'], record['overrides']) assert replay.placed == a.placed def test_semantic_property_comparison_and_overwrite(artifact_dir): left = 'minecraft:oak_stairs[facing=north,half=bottom,shape=straight]' reordered = 'minecraft:oak_stairs[shape=straight,half=bottom,facing=north]' assert blockstates_equivalent(left, reordered) assert not blockstates_equivalent(left, left.replace('north', 'south')) assert not blockstates_equivalent(left, 'minecraft:oak_stairs[facing=north,half=bottom]') assert not blockstates_equivalent('minecraft:stone', 'minecraft:cobblestone') build = Build(31).build() build.put(0, 0, 0, 'minecraft:dirt') build.put(0, 0, 0, 'minecraft:oak_stairs', facing='north', half='bottom') build.export(artifact_dir / 'overwrite.litematic') build.verify(artifact_dir / 'overwrite.litematic') with pytest.raises(AssertionError, match='out of bounds'): build.put(-1, 0, 0, 'minecraft:stone') @pytest.mark.parametrize('params', [ { 'width': 16 }, { 'floors': True }, { 'unknown': 1 }, { 'floors': 1, 'wing_floors': 2 }, { 'floors': 1, 'balcony': True }, { 'topology': 'courtyard', 'porch': 'wrap' }, ]) def test_invalid_explicit_parameters(params): with pytest.raises(ValueError): resolve(0, params) @pytest.mark.parametrize('seed', [True, -(2**63) - 1, 2**63, '7']) def test_seed_domain(seed): with pytest.raises(ValueError, match='signed 64-bit'): resolve(seed) def test_resource_limits_are_enforced(): for limits in ({'max_axis': False}, {'max_height': -1}, {'tree_height': [4, 50]}, {'tree_height': [25, 20]}): with pytest.raises(ValueError): resolve(0, limits=limits) with pytest.raises(ValueError, match='resource limits'): resolve(0, limits={'max_axis': 20}) with pytest.raises(ValueError, match='floor_height'): resolve(0, limits={'floor_height': 5}) with pytest.raises(ValueError, match='unknown resource limit'): resolve(0, limits={'hidden': 100}) with pytest.raises(ValueError, match='not a Java'): blockstate('unobtainium') with pytest.raises(ValueError, match='invalid'): blockstate('oak_stairs', (('facing', 'up'),)) def test_observed_structural_diversity(): signatures = set() coverage = {k: set() for k in ('topology', 'floors', 'ridge', 'profile', 'tower', 'conservatory', 'porch', 'balcony', 'dormers', 'terrain', 'entrance', 'layout', 'stair_side')} for seed in range(40): b = Build(seed).build() signatures.add(geometry_signature(b.scene)) for key in coverage: coverage[key].add(b.plan.parameters[key]) assert len(signatures) == 40 for key, observed in coverage.items(): assert observed == set(DOMAINS[key]), (key, observed) # Change a genuine room-division parameter under the same massing. params = dict(TARGETS[0][1], floors=2) a = Build(31, dict(params, layout='open')).build() b = Build(31, dict(params, layout='suites')).build() assert a.plan.signature() == b.plan.signature() assert geometry_signature(a.scene) != geometry_signature(b.scene) @pytest.mark.parametrize('defect', ['door', 'bed', 'stair_headroom', 'ladder_support', 'window', 'entry']) def test_actual_geometry_defects_are_rejected(defect): scene = Build(DEFAULT_SEED).build().scene if defect == 'door': x, y, z = scene.doors[0] scene.p(x, y + 1, z, 'air') elif defect == 'bed': x, y, z = scene.beds[0] scene.p(x, y, z, 'air') elif defect == 'stair_headroom': x, y, z, _ = scene.stairs[-1] scene.p(x, y + 2, z, 'stone') elif defect == 'ladder_support': x, y, z = scene.ladders[0] _, props = parse(scene.blocks[x, y, z]) dx, dz = DIRECTIONS[props['facing']] scene.p(x - dx, y, z - dz, 'air') elif defect == 'window': scene.p(*scene.windows[0]['cells'][0], 'stone') else: x, y, z = scene.plan.entrance scene.box((x, y, z, x, y + 2, z), 'stone') with pytest.raises(AssertionError): validate_scene(scene) def test_envelope_defect_is_rejected(): scene = Build(907, TARGETS[0][1]).build().scene v = scene.plan.volumes[0] x, z = (v.x0 + v.x1) // 2, (v.z0 + v.z1) // 2 h = v.roof(x, z) scene.clear((x, h - 2, z, x, h + 3, z)) assert envelope_leaks(scene) def test_cli_clean_directory(artifact_dir): script = Path(__file__).resolve().parents[1] / 'generate.py' subprocess.run([sys.executable, str(script), '--seed', '31'], cwd=artifact_dir, check=True, capture_output=True) assert (artifact_dir / 'output.litematic').is_file() assert json.loads((artifact_dir / 'output.json').read_text())['seed'] == 31

14 · 浏览器版本与实测边界

本页使用 Pyodide 0.27.7 和 module Web Worker。三个核心模块保持与实验交付文件逐字节一致,加载前校验 SHA-256。 原代码读取的方块状态 JSON 被装入虚拟文件系统的原路径;建筑生成算法没有翻译为 JavaScript。

Python 生成并验证方块后,将 palette 与整数坐标数组发给页面,由现有 MCRender 生成带真实方块模型和纹理的预览。 Worker 隔离了 Python 计算;网格构建仍在页面线程,大型建筑可能短暂卡顿。取消会终止 Worker,下次重新初始化。

浏览器 schematic 导出是报告新增的标准库适配器,保持建筑方块语义;文件 metadata、palette 顺序及压缩实现可能与原 MCIO 文件不同,不能要求两种导出逐字节相同。 剖切和隐藏选项仅影响预览,下载始终是完整建筑。

浏览器兼容性与互操作验证已完成。实测记录

测试结果
浏览器Headless Chrome 153 / Linux x86_64
11 份样本重生成11/11 结构签名与原实验样本匹配,完整几何验证通过
下载互操作11/11 浏览器 schematic 经 MCIO 加载,与原样本逐格比较方块名和属性全部相同
Python 耗时预热后 11 样本约 0.12–0.48 秒,包含生成、完整验证和序列化
预览网格耗时同次 11 样本约 0.15–0.58 秒,页面线程,未包含首屏资源下载
确定性相同 seed 与参数重复生成的完整方块摘要一致
64 位 seed两个端点均成功且字符串传输保持整数精度,越界 seed 被拒绝
不合法组合floors=1 与 balcony=true 被拒绝,已有成功预览保留
取消与恢复终止 Worker 后可重新初始化并成功生成
链接与移动布局分享链接重载后恢复 seed;375px 视口无整页横向溢出,剖切与材质隐藏可用

耗时为本次测试浏览器的测量值,随设备与参数变化,不是性能保证。

首次打开需要网络下载运行时和静态资源;缓存是否保留由浏览器决定,本页不承诺离线运行。 需要 WebGL2。渲染无法反映游戏中的粒子、着色器光照与全部 block entity 效果。

Pyodide Worker 文档 · 自定义 Python 模块加载

15 · 证据与复现

python generate.py --seed 21 --set topology=ell --set floors=2 python -m pytest -q test python sample_batch.py python render_batch.py

上面命令使用原实验的 Python / MCIO / MCRender 环境。网页演示、导出适配器和本报告属于实验后的展示工作,不计入 Codex 实验的时间、token 和原始测试结果。


Hanke Chen · Koke_Cacao · 证据优先,结论保留边界。