Contents

八个任务,一档 effort


同一个容器、同一套工具、同一档 effort=xhigh,八个不同的建筑任务各跑一次。 这一轮换的自变量不是 effort 而是任务本身:每个 prompt 只给一张参考图,要求模型自己设计室内。 八个全部通过,问题不在能不能做出来,而在于八次的开销、结构与失败方式有多像。

8/8通过
$4.640八次合计(估算)
10.9M总 token
2h21m总墙钟

关键观察

从表面到深层排列。数字都来自下面各节的表和图。

  1. 八个任务全部一次通过,没有一个需要人工救场。产物有效性已经不是这一轮的瓶颈。
  2. 八次合计估算 $4.640,平均每个建筑 $0.580。输出 token 占掉 56% 的费用,而它只占 token 总量的 2.4%。
  3. 墙钟时间几乎就是输出 token 数,两者相关系数 r=+1.00。想缩短一次 run 只有压推理长度这一条路,少读几个文件、少跑几条命令都动不了这根针。
  4. 缓存命中占输入的 93.8%。八次里新增输入合计只有 65.5 万 token,其余 996 万都是重复喂进去的同一份上下文。
  5. 建筑体积差 15.6 倍(3,360 格到 52,503 格),费用只差 1.65 倍。费用跟生成器代码行数走(r=+0.77),不跟建筑体积走(r=+0.33)。模型的开销由它写了多少代码决定,不由它建了多大东西决定。
  6. 测试规模是个常数:八次都落在 286–419 行、9–14 个测试函数,变异系数 13%,而体积的变异系数是 66%。测试数量甚至和体积轻微负相关(r=-0.43)。模型对『测试该写多少』有个固定预算,不随任务难度调整。
  7. 调色板丰富度和生成器行数的相关性 r=+0.86,和方块数 r=+0.88。材质丰富不是审美决定,是多写代码的副产品。
  8. 八条轨迹的阶段序列几乎同构:探索 → 写生成器 → 渲染 → 写测试 → 交付,中间反复在渲染和测试之间摆动。这套 AGENTS.md 已经把解题路径固化成了一条流水线。
  9. 23 次失败里 18 次是自己写的测试没过。模型给自己定的验收标准比它第一版实现更严,这是它自我纠错的主要入口。
  10. 3 次失败是模板 verify() 报的 changed on reload:同一坐标被写了两次,内存里的记录和导出的文件对不上。八个任务里有三个踩到同一个坑,说明这是模板该挡而没挡住的。
  11. 唯一用 CLI 内置 patch 工具改文件的是 stone-cottage,其余七个都用 shell heredoc 把代码拼进 generate.py。它也是八个里唯一把 27k 新增输入花在『写生成器』阶段的——工具选择改变 token 的去向,不改变总量。
  12. 八个 prompt 用的是同一个模板,只换参考图和建筑描述。产出的代码结构、函数命名、测试命名高度一致,说明这一轮量到的差异主要来自任务本身,而不是 prompt 措辞。

实验设置

镜像 mcgen:latest,工具集 standard/opt/tools 四件套 + AGENTS.md), 模型 gpt-6-astraeffort=xhigh,一轮 user turn,无网页检索。 前四个由本人手动跑,后四个由 agent 代跑,命令完全相同。

./run_agent.sh root-house --model gpt-6-astra --effort xhigh ./run_agent.sh sandstone-cottage --model gpt-6-astra --effort xhigh ./run_agent.sh stone-cottage --model gpt-6-astra --effort xhigh ./run_agent.sh red-roof-farmstead --model gpt-6-astra --effort xhigh ./run_agent.sh stilt-water-cabin --model gpt-6-astra --effort xhigh ./run_agent.sh stilted-manor --model gpt-6-astra --effort xhigh ./run_agent.sh merchant-manor --model gpt-6-astra --effort xhigh ./run_agent.sh mediterranean-villa --model gpt-6-astra --effort xhigh

容器内实际执行: codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox -m gpt-6-astra -c model_reasoning_effort="xhigh" --image=<参考图> -- <PROMPT.md>

八个 prompt

同一份 PROMPT.md 模板,换掉三处:标题、参考图文件名、参考图的文字描述。 交付要求、坐标约定、确定性要求、测试要求八份逐字相同。每个任务只给一张参考图,室内由模型自己设计。

参考图prompt任务产物尺寸 X×Y×Z
root-house 参考图 root-house small dwelling built into a hillside under a mass of thick dark wood roots shown in the reference image 25×23×22
sandstone-cottage 参考图 sandstone-cottage wide cottage on a slope, with a pale stair roof and a half sunken lower floor shown in the reference image 32×24×26
stone-cottage 参考图 stone-cottage small cottage of stone walls and wooden corner posts under a pale stair roof shown in the reference image 16×15×14
red-roof-farmstead 参考图 red-roof-farmstead farmhouse with a steep red roof, standing on a square plot with a well, a field and a windmill shown in the reference image 42×29×42
stilt-water-cabin 参考图 stilt-water-cabin an all wood cabin standing on posts over shallow water, with a deck and a mooring shown in the reference image 28×21×24
stilted-manor 参考图 stilted-manor tall medieval house whose upper floors overhang a stone ground floor on heavy wooden posts shown in the reference image 33×39×31
merchant-manor 参考图 merchant-manor tall medieval merchant house with a stone undercroft, a market stall and steep dark roofs shown in the reference image 37×43×33
mediterranean-villa 参考图 mediterranean-villa two storey white and blue villa with terracotta roofs, terraces, a pergola and a pool shown in the reference image 35×27×31
PROMPT.md 里八份共用的那一半(以 root-house 为例)
Structure and scale - Java 1.21.1, vanilla blocks, one region, one schematic. - Scale: roughly 25 by 22 blocks of ground and about 22 blocks tall. Keep the footprint at that scale so the interiors stay walkable. - The mass reads as roots, not as a box: the wood spreads outward and downward from a central trunk, and the ends curl or step up. Nothing is a flat rectangular wall. - The hillside behind stays visible as stone and mossy cobblestone between the wooden ribs. - The entrance is a cave mouth framed by the roots, with the porch canopy and the hanging lantern in front. Materials - Dark wood family for the roots: dark oak logs, stripped dark oak, dark oak planks, spruce for contrast. - Hillside family: stone, cobblestone, mossy cobblestone, andesite. - Vary each family with two or three related blocks so no surface is a single flat texture. Interior The image shows only the outside. Design the inside yourself, and make it a home that fits the shell: - One or two levels of usable space hollowed out of the hill behind the entrance. - A sleeping area, a hearth or cooking area, storage, and a work corner, each furnished with vanilla blocks. - Root timbers from the outside continue inside as exposed beams and posts. - Lanterns, candles or a campfire carry the same warm light the porch lantern gives outside. ## Correctness, not just looks - Every floor and room is reachable on foot from the front door, with at least two blocks of headroom on the path. - Doors are complete, with matching lower and upper halves and the same facing and hinge. - Beds are complete, with head and foot adjacent and facing the same way. - The envelope is sealed, so every interior air pocket is separated from the sky. - Every decoration rests on or attaches to a solid block. - Windows and doorways stay clear: roof, chimney and furniture geometry keeps out of the openings. - Every room has a light source within a few blocks of its floor. ## Deliverables All inside `/work/generator/workspace`: - `output.litematic`: the finished build. This exact path is the contract. - the generator script that produced it, rerunnable from a clean directory with no arguments. `generate.py` already sits in the workspace, seeded from `/opt/tools/template_generator.py` when that template is present, and gives you bounds checked placement, a seeded random source and a byte reproducible export for free. Build on whichever tools `/opt/tools` actually holds. - `test/test_*.py`: structural tests covering the correctness rules above, passing. - `previews/`: the renders you used to check the build. Spend your budget on the build, the renders and the tests. The run trace is read directly, so the work itself is the record. You may edit any file in this directory, including this brief. These are your copies. The environment, the available libraries, the rendering tools and the determinism rules are described in `AGENTS.md`, which sits in your working directory.

对比表

点表头排序。token 分成三列:新增输入、缓存输入、输出(输出含推理)。

build请求命令 新增输入缓存输入输出其中推理 尺寸体积方块palette方块种类 生成器行测试数测试行失败估算
root-house 1319 30 28 104,524 1,830,528 39,867 18,365 25×23×22 12,650 3,986 77 52 632 11 360 3 $0.758
sandstone-cottage 755 19 25 101,211 780,416 23,353 6,942 32×24×26 19,968 6,777 103 72 558 14 313 2 $0.458
stone-cottage 868 24 24 56,248 1,052,288 25,824 6,325 16×15×14 3,360 883 65 44 407 11 331 2 $0.460
red-roof-farmstead 955 20 20 67,015 990,080 28,926 10,954 42×29×42 51,156 9,152 113 73 589 10 346 3 $0.497
stilt-water-cabin 854 22 21 66,787 1,085,824 25,710 9,214 28×21×24 14,112 3,603 68 40 459 10 397 2 $0.476
stilted-manor 1232 25 28 73,507 1,300,224 37,770 15,878 33×39×31 39,897 7,492 105 64 678 9 286 3 $0.632
merchant-manor 1300 27 31 104,885 1,738,880 39,277 13,468 37×43×33 52,503 7,401 132 78 850 10 408 5 $0.741
mediterranean-villa 1184 20 25 81,215 1,185,152 36,822 13,625 35×27×31 29,295 7,411 120 87 782 11 419 3 $0.618
合计8467187202 655,3929,963,392257,549 46,705 4,955862,860 23$4.640

八次全部 passed=trueschematic_valid=truepytest exit=0, 所以这三列没有放进表里。


图表

每张图只放同一量纲的东西。token、秒、方块数、行数不混在一张图里。

Fig 1. 墙钟耗时(秒)。
Fig 2. 模型请求数与 shell 命令数(次)。
Fig 3. 新增输入与输出 token。缓存输入量级差一个数量级,单独画在 Fig 4。
Fig 4. 缓存命中的输入 token。八次合计 996 万,是新增输入的 15 倍。
Fig 5. 产物规模:非空气方块数与包围盒体积(格)。
Fig 6. 调色板状态数与方块种类数(个)。
Fig 7. 生成器行数与测试行数(行)。注意测试行数几乎是一条平线。
Fig 8. 费用与两个候选解释变量:生成器行数(r=+0.77)比建筑体积(r=+0.33)解释力强得多。 横轴做了归一化(各自除以本列最大值),纵轴是估算美元。

schematic

八个产物本体,可拖动旋转。文件就是 out/output.litematic 的原件。

root-house — 25×23×22,3,986 方块,77 个状态

sandstone-cottage — 32×24×26,6,777 方块,103 个状态

stone-cottage — 16×15×14,883 方块,65 个状态

red-roof-farmstead — 42×29×42,9,152 方块,113 个状态

stilt-water-cabin — 28×21×24,3,603 方块,68 个状态

stilted-manor — 33×39×31,7,492 方块,105 个状态

merchant-manor — 37×43×33,7,401 方块,132 个状态

mediterranean-villa — 35×27×31,7,411 方块,120 个状态


渲染图

每组第一张是参考图,其余是模型在 run 中渲染并看过的图。所有渲染都走同一份 template_render.mjs 的光照参数。

root-house

7 张渲染,全部是模型自己跑出来看过的。

root-house reference
参考图(输入)
root-house entrance
entrance
root-house front
front
root-house ground_floor
ground_floor
root-house loft
loft
root-house rear
rear
root-house side
side
root-house top
top

sandstone-cottage

6 张渲染,全部是模型自己跑出来看过的。

sandstone-cottage reference
参考图(输入)
sandstone-cottage cellar_interior
cellar_interior
sandstone-cottage front
front
sandstone-cottage rear
rear
sandstone-cottage side
side
sandstone-cottage top
top
sandstone-cottage upper_interior
upper_interior

stone-cottage

6 张渲染,全部是模型自己跑出来看过的。

stone-cottage reference
参考图(输入)
stone-cottage front
front
stone-cottage interior
interior
stone-cottage loft
loft
stone-cottage rear
rear
stone-cottage side
side
stone-cottage top
top

red-roof-farmstead

6 张渲染,全部是模型自己跑出来看过的。

red-roof-farmstead reference
参考图(输入)
red-roof-farmstead front
front
red-roof-farmstead ground_floor
ground_floor
red-roof-farmstead loft
loft
red-roof-farmstead rear
rear
red-roof-farmstead side
side
red-roof-farmstead top
top

stilt-water-cabin

5 张渲染,全部是模型自己跑出来看过的。

stilt-water-cabin reference
参考图(输入)
stilt-water-cabin front
front
stilt-water-cabin interior
interior
stilt-water-cabin rear
rear
stilt-water-cabin side
side
stilt-water-cabin top
top

stilted-manor

8 张渲染,全部是模型自己跑出来看过的。

stilted-manor reference
参考图(输入)
stilted-manor attic_interior
attic_interior
stilted-manor front
front
stilted-manor ground_interior
ground_interior
stilted-manor hero
hero
stilted-manor living_interior
living_interior
stilted-manor rear
rear
stilted-manor side
side
stilted-manor top
top

merchant-manor

9 张渲染,全部是模型自己跑出来看过的。

merchant-manor reference
参考图(输入)
merchant-manor approach
approach
merchant-manor bedrooms
bedrooms
merchant-manor front
front
merchant-manor hall
hall
merchant-manor rear
rear
merchant-manor side
side
merchant-manor study
study
merchant-manor top
top
merchant-manor undercroft
undercroft

mediterranean-villa

7 张渲染,全部是模型自己跑出来看过的。

mediterranean-villa reference
参考图(输入)
mediterranean-villa first_floor
first_floor
mediterranean-villa front
front
mediterranean-villa rear
rear
mediterranean-villa second_floor
second_floor
mediterranean-villa side
side
mediterranean-villa stair_section
stair_section
mediterranean-villa top
top

命令轨迹

把每个模型请求按它触发的最强动作打一个标签,八条轨迹就能压成八个字符串。 字母含义:E 探索、G 写生成器、R 渲染、T 测试、 F 修补格式化、V 验证导出、P 纯推理或说明。

build阶段序列(按请求顺序)请求数
root-houseEGEEGRVGTRTPTPTRVEVETRVVTEPTPP30
sandstone-cottageEEGEGGRGPRTRTPTTPEP19
stone-cottageEGEGPGFGRPPREGGTGTRRPTPP24
red-roof-farmsteadEGEGRVPRRTPVTEPTRRPP20
stilt-water-cabinEEGPEGFGRPPRRTTPVTRTPP22
stilted-manorEGGRVPGVTVTRVTRRPTPTTRPTP25
merchant-manorEGEEPGRVPRTTVVTRTRTTTRTRTPP27
mediterranean-villaEGEEPGRRGPRTRTTETRPP20

八条轨迹的共同形状:E 开头一到三步摸环境,紧接第一次 G 落地生成器骨架, 然后 R 出现,此后在 RT 之间来回,最后以 P 收尾。 没有一个 run 是先写完测试再建东西的,也没有一个是建完就交付不渲染的。

同一个请求里如果既改了生成器又跑了 pytest,按『最强动作』规则算测试。 这就是 F 在多数 build 里是 0 的原因:yapf 几乎总是和别的动作串在同一条命令里。

完整命令列表

root-house — 28 条命令,3 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g 'test*' -g '*.py' -g '*.mjs' -g '*.jpg' . /work 2>/dev/null | head -70 [01] cat /work/AGENTS.md && cat AGENTS.md && cat /opt/tools/template_generator.py && cat /opt/tools/template_render.mjs [02] cp /opt/tools/template_generator.py generate.py && cp /opt/tools/template_render.mjs render.mjs && /opt/venv/bin/python - <<'PY' import json from pathlib import Path p=Path('/work/generator/MCRender/resources/1.21.1-blocks.json') data=json.loads(p.read_text()) print(type(data), str(data)[:2000]) PY [03] rg -n 'block_entities|entities|tile_entities|class.*Litematica|class.*Region|class.*Compound' /work/generator/MCIO/mcio -g '*.py' | head -80 && cat /opt/tools/cutaway.py [04] sed -n '636,840p' /work/generator/MCIO/mcio/litematica.py && rg -n 'entities|tile_entities|TileEntities|Entities|to_litematica' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/litematica.py | head -75 && sed -n '1088,1125p' /work/generator/MCIO/mcio/nbt/tag.py [05] sed -n '845,900p' /work/generator/MCIO/mcio/litematica.py && sed -n '130,230p' /work/generator/MCIO/mcio/nbt/nbt.py && sed -n '100,170p' /work/generator/MCIO/mcio/sketch.py [06] mkdir -p test previews && cat > /tmp/root_build_methods.txt <<'PY' def build(self) -> "Build": """Build the rooted hillside, two furnished floors, and planted forecourt. Returns: Build: Completed build ready for export. """ self.terrain() self.roots() self.rooms() self.front() self.furnish() self.garden() return self def terrain(self) -> None: """Raise an irregular stone hill above a rounded woodland ground patch.""" for x in range(self.size_x): for z in range(self.size_z): if ((x - 12) / 13.0)**2 + ((z - 11) / 12.2)**2 > 1.25: continue self.put(x, 0, z, self.rng.choice(GROUND_BASE)) self.put(x, 1, z, self.rng.choice(GROUND_TOP)) if z < 7: continue radial = ((x - 12) / 11.0)**2 + ((z - 13.5) / 9.0)**2 if radial >= 1: continue height = 2 + int(12.5 * (1 - radial)**0.58) for y in range(2, height + 1): palette = ROCK if y < height else HILL_TOP ...(截断) [07] chmod u+w generate.py render.mjs && /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('import argparse','import argparse\nimport gzip\nimport io') s=s.replace('from mcio.schematic import load_schematic', 'from mcio.nbt.nbt import File as NBTFile\nfrom mcio.nbt.tag import Compound, Int, Byte, String, List as NBTList\nfrom mcio.schematic import load_schematic') s=s.replace('"size_xyz": (45, 40, 35)', '"size_xyz": (25, 23, 22)').replace('"name": "build"','"name": "Root House"').replace('"description": "Generated build"', '"description": "A hearth and sleeping loft sheltered beneath ancient dark oak roots. Front faces north (-Z)."') pos=s.index('\nPosition =') s=s[:pos]+''' AIR = 'minecraft:air' ROCK = ['minecraft:stone'] * 4 + ['minecraft:cobblestone'] * 3 + ['minecraft:mossy_cobblestone'] * 3 + ['minecraft:andesite'] HILL_TOP = ['minecraft:moss_block'] * 5 + ['minecraft:mossy_cobblestone'] * 3 + ['minecraft:stone', 'minecraft:rooted_dirt'] GROUND_BASE = ['minecraft:dirt'] * 5 + ['minecraft:rooted_dirt'] * 2 + ['minecraft:stone'] GROUND_TOP = ['minecraft:grass_block'] * 7 + ['minecraft:moss_block'] * 2 + ['minecraft:coarse_dirt'] PATH ...(截断) [08] /opt/venv/bin/python - <<'PY' from generate import Build, load_schematic b=Build().build(); s=load_schematic('output.litematic'); a=s.read_flat(0,s.volume).reshape(s.size_yzx) for (x,y,z), state in b.placed.items(): if s.palette[int(a[y,z,x])]!=state: print((x,y,z),state, s.palette[int(a[y,z,x])]) PY [09] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('return self.canvas.material(name, **properties)', 'return self.canvas.material(name, **dict(sorted(properties.items())))');p.write_text(s) PY /opt/venv/bin/python generate.py [10] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text();s=s.replace("self.put(19, 9, 15, 'minecraft:dark_oak_planks')\n self.put(18, 10, 15, 'minecraft:dark_oak_slab', type='top', waterlogged='false')\n self.put(18, 9, 15, 'minecraft:lantern', hanging='true', waterlogged='false')", "self.put(17, 11, 16, 'minecraft:dark_oak_planks')\n self.put(17, 10, 16, 'minecraft:lantern', hanging='true', waterlogged='false')") s=s.replace("self.put(9 + dx, 15, 18 + dz, 'minecraft:spruce_trapdoor'", "self.put(9 + dx, 14, 18 + dz, 'minecraft:cobblestone')\n self.put(9 + dx, 15, 18 + dz, 'minecraft:spruce_trapdoor'") p.write_text(s) PY cat > test/test_structure.py <<'PY' # Validate the exported Root House, including movement, support, enclosure, and deterministic bytes. from __future__ import annotations import gzip import hashlib import json import shutil import subprocess import sys import tempfile from collections import deque from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, '/work/generator/MCIO') from generate import MAIN_ROWS, LOFT_ROWS from ...(截断) [11] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/ground_floor.litematic --max-y 5 && /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/loft.litematic --max-y 10 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_floor.litematic --output previews/ground_floor.png --size 900 --azimuth 210 --elevation 65 --radius 2.5 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/loft.litematic --output previews/loft.png --size 900 --azimuth 210 --elevation 60 --radius 2.8 [12] # exit 1 sed -i 's/NBTFile.from_buffer(stream).root/NBTFile.parse(stream)/' test/test_structure.py /opt/venv/bin/python -m pytest -q test [13] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('test/test_structure.py');s=p.read_text().replace("self.get(add(p, (0, 1, 0)))[0] in AIR", "(self.get(add(p, (0, 1, 0)))[0] in AIR or self.get(add(p, (0, 1, 0)))[0].endswith('_door'))");p.write_text(s) PY /opt/venv/bin/python -m pytest -q test/test_structure.py::test_every_room_and_floor_reachable_with_headroom [14] rg -n 'block_entities|def build|def export|small daylight|barrel.*water|self.garden|Position =|def test_window|destinations|zones =' generate.py test/test_structure.py [15] cat > /tmp/orient_root.txt <<'PY' def orient_front(self) -> None: """Reflect the plan so the leaf crown appears left of the north-facing door.""" placed = list(self.placed.items()) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed = {} for (x, y, z), state in placed: name, _, suffix = state.partition('[') props = dict(item.split('=') for item in suffix.rstrip(']').split(',') if item) if props.get('facing') in ('east', 'west'): props['facing'] = {'east': 'west', 'west': 'east'}[props['facing']] if 'east' in props and 'west' in props: props['east'], props['west'] = props['west'], props['east'] if 'hinge' in props: props['hinge'] = {'left': 'right', 'right': 'left'}[props['hinge']] if props.get('shape', '').endswith(('_left', '_right')): part, side = props['shape'].rsplit('_', 1) props['shape'] = part + '_' + {'left': 'right', 'right': 'left'}[side] self.put(self.size_x - 1 - x, y, z, name, **props) for entity in self.block_entities: ...(截断) [16] RENDER_SIZE=1000 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [17] /opt/venv/bin/python - <<'PY' import gzip from generate import NBTFile, load_schematic s=load_schematic('output.litematic');ids=s.read_flat(0,s.volume) print('Actual non-air blocks:', sum(int(i)!=0 for i in ids)) with gzip.open('output.litematic','rb') as f: root=NBTFile.parse(f) print('Metadata total:', root['Metadata']['TotalBlocks']) PY [18] /opt/venv/bin/python - <<'PY' from generate import Build b=Build().build() for x,z in [(16,7),(17,7),(17,8),(18,8),(16,8),(18,7),(17,9),(18,9)]: ys=[(y,b.get(x,y,z)) for y in range(23) if b.get(x,y,z) not in (None,'minecraft:air')] print((x,z),ys[-3:]) PY [19] /opt/venv/bin/python - <<'PY' import sys sys.path.insert(0,'test') from test_structure import World,ROOT,MAIN_ROWS,LOFT_ROWS w=World(ROOT/'output.litematic') lights=[p for p,n,a in w.cells() if n=='lantern' or (n in {'campfire','candle'} and a.get('lit')=='true')] for rows,y in [(MAIN_ROWS,3),(LOFT_ROWS,8)]: ds=[] for z,(a,b) in rows.items(): for x in range(a,b+1): p=(x,y,z) if w.standable(p):ds.append((min(sum(abs(i-j) for i,j in zip(p,q)) for q in lights),p)) print(sorted(ds,reverse=True)[:8]) PY [20] /opt/venv/bin/python - <<'PY' from generate import Build b=Build().build() for p in [(20,2,8),(20,3,8),(20,4,8),(19,2,8),(21,2,8),(20,2,7),(20,2,9)]:print(p,b.get(*p)) PY [21] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace("self.put(6, 3, 16, 'minecraft:furnace'", "self.put(6, 3, 14, 'minecraft:furnace'") s=s.replace("self.put(15, 5, 10, 'minecraft:lantern', hanging='true', waterlogged='false')", "self.put(15, 5, 10, 'minecraft:lantern', hanging='true', waterlogged='false')\n self.put(16, 6, 14, 'minecraft:dark_oak_log', axis='x')\n self.put(16, 5, 14, 'minecraft:lantern', hanging='true', waterlogged='false')") s=s.replace("[(7, 10, 'sweet_berry_bush'),", "[(7, 8, 'sweet_berry_bush'), (8, 8, 'sweet_berry_bush'), (7, 10, 'sweet_berry_bush'),") s=s.replace("self.put(4, 2, 8, 'minecraft:composter', level='7')", "self.put(2, 2, 7, 'minecraft:composter', level='7')") p.write_text(s) p=Path('test/test_structure.py');s=p.read_text() s=s.replace("valid = world.get(above)[0] not in AIR if props['hanging'] == 'true' else world.top_support(below)", "valid = world.full(above) if props['hanging'] == 'true' else world.top_support(below)") s=s.replace("elif name in {'candle', 'flower_pot', 'campfire', 'water_cauldron', 'composter'}", "elif name in {'candle', 'flower_pot', 'campfire', 'water_cauldro ...(截断) [22] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/ground_floor.litematic --max-y 5 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/loft.litematic --max-y 10 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_floor.litematic --output previews/ground_floor.png --size 1000 --azimuth 210 --elevation 65 --radius 2.8 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/loft.litematic --output previews/loft.png --size 1000 --azimuth 210 --elevation 60 --radius 2.9 [23] /opt/venv/bin/python - <<'PY' import sys sys.path.insert(0,'test') from test_structure import World,ROOT,walk_reachable,DIRECTIONS,add w=World(ROOT/'output.litematic');r=walk_reachable(w) for x in range(19,25): for z in range(3,10): p=x,2,z print(p, w.get(p)[0],w.get(add(p,(0,1,0)))[0],w.get(add(p,(0,-1,0)))[0], 'reachable' if p in r else '') PY [24] /opt/venv/bin/python - <<'PY' import sys sys.path.insert(0,'test') from test_structure import * w=World(ROOT/'output.litematic');r=walk_reachable(w) for p in [(21,2,1),(20,2,1),(18,2,3),(19,2,1),(22,2,2),(17,2,2)]:print(p,w.get(p),w.get(add(p,(0,1,0))),p in r,[add(p,d) for d in DIRECTIONS.values() if add(p,d) in r]) PY [25] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('[(7, 10), (7, 16), (17, 9), (17, 18)]', '[(7, 10), (6, 16), (17, 9), (17, 18)]').replace("self.put(2, 2, 7, 'minecraft:composter'", "self.put(6, 2, 3, 'minecraft:composter'") p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py && /opt/venv/bin/python -m pytest -q test [26] sed -n '1,75p' generate.py && sed -n '540,625p' generate.py && rg --files -g '!__pycache__/**' -g '!test/__pycache__/**' -g '!.pytest_cache/**' . | sort [27] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text();start=s.index('from __future__');s='# Generate and verify a furnished Java 1.21.1 Root House with deterministic block and entity data.\n'+s[start:];p.write_text(s) p=Path('render.mjs');s=p.read_text().replace('// Starting point for rendering a build. Copy into the workspace and edit VIEWS.', '// Render five exterior inspection views of the Root House.');p.write_text(s) PY cat > README.md <<'MD' # Root House `output.litematic` is the finished, furnished Minecraft Java 1.21.1 schematic. One region, 25 blocks wide by 22 deep by 23 high including its woodland ground base. The entrance faces north (-Z). The ground floor contains a hearth, kitchen, dining table, storage, and workbench. A two-wide stair leads to the sleeping loft and writing nook. Outside are curling dark oak roots, a lantern porch, berry bushes, vines, a leaf crown, log seats, a composter, and a barrel with a bucket item frame and water pail. Regenerate and verify from this directory: ```sh /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node ...(截断)
sandstone-cottage — 25 条命令,2 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g '*.jpg' . /work | head -80 [01] cat /work/AGENTS.md && cat /opt/tools/template_generator.py && cat /opt/tools/template_render.mjs [02] cat AGENTS.md && sed -n '1,230p' /work/generator/MCIO/mcio/sketch.py [03] rg -n 'entities|block_entities|class .*Entity|tile_entities' /work/generator/MCIO/mcio -g '*.py' | head -80 && head -c 1200 /work/generator/MCRender/resources/1.21.1-blocks.json [04] rg -n 'class Litematica|Entities|TileEntities|def save|def to_nbt' /work/generator/MCIO/mcio/litematica.py && sed -n '1,180p' /work/generator/MCIO/mcio/nbt/nbt.py && sed -n '720,910p' /work/generator/MCIO/mcio/litematica.py [05] cp /opt/tools/template_generator.py generate.py && cp /opt/tools/template_render.mjs render.mjs [06] sed -n '1310,1355p' /work/generator/MCIO/mcio/litematica.py && rg -n 'def write_to_nbt|def save|gzip' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt/nbt.py | tail -25 [07] cat > /tmp/build_body.py <<'PY' def stair(self, x: int, y: int, z: int, block: str, facing: str, half: str = "bottom") -> None: """Place a stair with explicit geometry and dry state.""" self.put(x, y, z, block, facing=facing, half=half, shape="straight", waterlogged="false") def slab(self, x: int, y: int, z: int, block: str, kind: str = "bottom") -> None: """Place a slab with explicit height.""" self.put(x, y, z, block, type=kind, waterlogged="false") def door(self, x: int, y: int, z: int, facing: str = "north", hinge: str = "left") -> None: """Place matching closed door halves on an existing floor.""" for half, dy in (("lower", 0), ("upper", 1)): self.put(x, y + dy, z, "minecraft:spruce_door", facing=facing, hinge=hinge, half=half, open="false", powered="false") def lantern(self, x: int, y: int, z: int, hanging: bool = False) -> None: """Place a lantern and remember its lighting location.""" self.put(x, y, z, "minecraft:lantern", hanging=str(hanging).lower(), waterlogged="false") def terrain(self) -> None: """Sculpt the rising riverbank, with exposed stone below the turf.""" ...(截断) [08] # exit 1 chmod u+w generate.py render.mjs cat > /tmp/adapt_generator.py <<'PY' from pathlib import Path p = Path('generate.py') s = p.read_text() a=s.index(' def build(') b=s.index(' def export(', a) s=s[:a]+Path('/tmp/build_body.py').read_text()+s[b:] s=s.replace('"size_xyz": (45, 40, 35)', '"size_xyz": (32, 24, 26)') s=s.replace('"name": "build"', '"name": "Willowbank Cottage"').replace('"description": "Generated build"', '"description": "A furnished sandstone-roof cottage above a river, with a sunken workshop, brick stair and timber jetty."') s=s.replace('Position = Tuple[int, int, int]', '''PLASTER = ["minecraft:smooth_sandstone"] * 8 + ["minecraft:sandstone"] * 2 + ["minecraft:cut_sandstone"] STONE = ["minecraft:stone_bricks"] * 5 + ["minecraft:andesite"] * 2 + ["minecraft:mossy_stone_bricks"] FLOOR = ["minecraft:spruce_planks"] * 7 + ["minecraft:oak_planks"] BRICK = ["minecraft:bricks"] * 7 + ["minecraft:granite", "minecraft:polished_granite"] Position = Tuple[int, int, int]''') s=s.replace('from mcio.schematic import load_schematic', 'from mcio.nbt.nbt import File as NBTFile\nfrom mcio.nbt.tag import Compound, String, Double, Float, Byte, Int, IntArray, List as NBTList\nfrom mc ...(截断) [09] /opt/venv/bin/python - <<'PY' from generate import Build, load_schematic from pathlib import Path b=Build().build(); s=load_schematic(Path('output.litematic')); ids=s.read_flat(0,s.volume).reshape(s.size_yzx) for p, state in b.placed.items(): x,y,z=p actual=s.palette[int(ids[y,z,x])] if actual != state: print(p,state,actual) break PY [10] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [11] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py'); s=p.read_text().replace('return self.canvas.material(name, **properties)', 'return self.canvas.material(name, **dict(sorted(properties.items())))'); p.write_text(s) PY /opt/venv/bin/python generate.py [12] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/cellar_slice.litematic --max-y 7 [13] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/upper_slice.litematic --max-y 12 [14] sed -n '1,150p' /opt/tools/render_schematic.mjs && /opt/venv/bin/python - <<'PY' import json from generate import Build r=json.load(open('/work/generator/MCRender/resources/1.21.1-blocks.json')) b=Build().build() for state in set(b.placed.values()): base=state.split('[')[0].split(':')[1] if base not in r: print('bad block',state); continue if '[' in state: for p in state[:-1].split('[')[1].split(','): k,v=p.split('=') if k not in r[base][0] or v not in r[base][0][k]: print('bad prop',state) PY [15] # exit 1 mkdir -p test cat > test/test_structure.py <<'PY' # Verify the saved cottage's geometry, circulation, decoration support and deterministic export. from collections import deque from pathlib import Path import json import subprocess import sys import tempfile import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG, NBTFile, load_schematic AIR = {'air', 'cave_air', 'void_air'} DIRECTIONS = {'north': (0, 0, -1), 'south': (0, 0, 1), 'west': (-1, 0, 0), 'east': (1, 0, 0)} FLOWERS = {'oxeye_daisy', 'cornflower', 'azure_bluet', 'short_grass', 'tall_grass', 'sweet_berry_bush', 'azalea'} LIGHTS = {'lantern', 'candle', 'campfire', 'torch'} def parse(state): """Split an exported state into block name and properties.""" name, _, suffix = state.partition('[') props = dict(p.split('=') for p in suffix.rstrip(']').split(',')) if suffix else {} return name.removeprefix('minecraft:'), props class Saved: """Offer coordinate access to blocks reloaded from the deliverable.""" def __init__(self, path): self.raw = NBTFile.load_regardless_of_gzipped(path) loaded = load_sche ...(截断) [16] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/cellar_slice.litematic --output previews/cellar_interior.png --size 768 --azimuth 210 --elevation 66 --radius 3.1 [17] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/upper_slice.litematic --output previews/upper_interior.png --size 768 --azimuth 210 --elevation 66 --radius 3.1 [18] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace('self.box((x, 0, z, x, max(0, h - 2), z), "minecraft:stone")', 'self.texture((x, 0, z, x, max(0, h - 2), z), ["minecraft:stone"] * 12 + ["minecraft:andesite", "minecraft:tuff"])') s=s.replace('self.slab(x, 1, 1, "minecraft:dark_oak_slab", "top")', 'for keel_z in range(3):\n self.slab(x, 1, keel_z, "minecraft:dark_oak_slab", "top")') s=s.replace('self.slab(x, 11, 17, "minecraft:spruce_slab")', 'self.slab(x, 11, 17, "minecraft:spruce_slab", "top")') s=s.replace('Double(9.3)', 'Double(3.0)') p.write_text(s) p=Path('test/test_structure.py');s=p.read_text().replace("('front_path', (19, 4, 4))", "('front_path', (19, 3, 4))").replace("'barrel': 20", "'barrel': 12") p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py test/test_structure.py /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_structure.py [19] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace(' self.lantern(12, 8, 19, hanging=True)', ' self.lantern(12, 8, 19, hanging=True)\n self.box((10, 8, 16, 14, 8, 16), "minecraft:stripped_spruce_log", axis="x")\n self.lantern(11, 7, 16, hanging=True)') s=s.replace('# Starting point for a Minecraft build generator. Copy into the workspace and fill in build().\n# Everything that has to be right for the harness is already right here: bounds checked placement,\n# one seeded random source, and a byte reproducible export.', '# Generate Willowbank Cottage with furnished rooms, terrain, waterfront and a usable boat.\n# Run with /opt/venv/bin/python generate.py from any writable directory.') p.write_text(s) p=Path('render.mjs');s=p.read_text().replace('?? 512', '?? 768').replace("name: 'front', azimuth: 210, elevation: 18, radius: 3.3", "name: 'front', azimuth: 200, elevation: 18, radius: 3.2") p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_structure.py /opt/venv/bin/python /opt/tools/cutaway.py --input output.litemati ...(截断) [20] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/cellar_slice.litematic --output previews/cellar_interior.png --size 768 --azimuth 210 --elevation 66 --radius 3.1 [21] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/upper_slice.litematic --output previews/upper_interior.png --size 768 --azimuth 210 --elevation 66 --radius 3.1 [22] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [23] cat > README.md <<'EOF' # Willowbank Cottage A furnished Java 1.21.1 cottage with a sandstone stair roof, a projecting gable, brick entrance steps, a sunken workshop and store, and a river jetty. The single region measures 32 x 24 x 26 blocks (X, Y, Z), including the terrain and riverbed. The front faces north (-Z). - `output.litematic`: complete build, including a usable spruce boat entity. - `generate.py`: seeded generator, derived from the supplied template. - `test/test_structure.py`: tests against the exported schematic. - `render.mjs`: exterior preview settings. - `previews/`: four exterior views and two interior cutaways. Regenerate from any writable working directory: ```sh /opt/venv/bin/python /work/generator/workspace/generate.py ``` Verify from this workspace: ```sh /opt/venv/bin/python -m pytest -q test ``` The tests validate block states, doors, beds, room access and headroom, the sealed envelope, window clearance, supported decorations, lighting, boat NBT and byte-for-byte reproducibility. The renderer omits entities, so the jetty also has a small timber skiff made from blocks. The usable boat is checked directly in the saved NBT. EOF [24] rg --files -g '!__pycache__/**' -g '!test/__pycache__/**' -g '!.pytest_cache/**' .
stone-cottage — 24 条命令,2 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g '!node_modules' . /work | head -80 [01] cat /work/AGENTS.md && cat /opt/tools/template_generator.py && cat /opt/tools/template_render.mjs [02] cat AGENTS.md && cp /opt/tools/template_generator.py generate.py && cp /opt/tools/template_render.mjs render.mjs [03] /opt/venv/bin/python - <<'PY' import json from pathlib import Path p=Path('/work/generator/MCRender/resources/1.21.1-blocks.json') a=json.loads(p.read_text()) print(type(a)); print(str(a)[:1200]) PY [04] /opt/venv/bin/python - <<'PY' import json from pathlib import Path b=json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for name in ['andesite_stairs','stone_stairs','cobblestone_stairs','spruce_trapdoor','lantern','chain','ladder','white_bed','glass_pane','vine','sunflower','campfire','barrel','flower_pot','potted_fern','grindstone']: print(name,b.get(name)) PY [05] rg -n 'block_entities|tile_entities|TileEntities|nbt' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio -g '*litemat*' -g '*sketch*' | head -70 [06] sed -n '1,110p' /work/generator/MCIO/mcio/nbt/nbt.py && sed -n '675,703p' /work/generator/MCIO/mcio/litematica.py && sed -n '1,100p' /work/generator/MCIO/mcio/nbt/tag.py [07] stat -c '%A %U %G %n' generate.py render.mjs . && id && chmod u+w generate.py render.mjs [08] # exit 1 /opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py [09] /opt/venv/bin/python - <<'PY' from generate import * b=Build().build(); s=load_schematic(Path('output.litematic')); a=s.read_flat(0,s.volume).reshape(s.size_yzx) for (x,y,z),v in b.placed.items(): q=s.palette[int(a[y,z,x])] if q!=v: print((x,y,z),v,q) if x>3: break PY [10] rg -n 'def material|def block\(|class Material|parse_blockstate' /work/generator/MCIO/mcio/sketch.py [11] /opt/venv/bin/python generate.py && RENDER_SIZE=768 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [12] sed -n '150,225p' /work/generator/MCIO/mcio/sketch.py && sed -n '310,385p' generate.py [13] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/interior.litematic --max-y 4 && xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/interior.litematic --output previews/interior.png --size 768 --azimuth 155 --elevation 65 --radius 3.2 [14] sed -n '1,65p' generate.py && sed -n '186,205p' generate.py [15] /opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py [16] # exit 1 /opt/venv/bin/python -m yapf -i generate.py test/test_cottage.py && /opt/venv/bin/python generate.py && /opt/venv/bin/python -m pytest -q test [17] /opt/venv/bin/python -m yapf -i generate.py test/test_cottage.py && /opt/venv/bin/python generate.py && /opt/venv/bin/python -m pytest -q test [18] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/interior.litematic --max-y 4 && /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/loft.litematic --max-y 8 [19] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [20] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/interior.litematic --output previews/interior.png --size 768 --azimuth 155 --elevation 65 --radius 3.2 [21] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/loft.litematic --output previews/loft.png --size 768 --azimuth 150 --elevation 66 --radius 3.2 [22] /opt/venv/bin/python - <<'PY' from pathlib import Path from generate import load_schematic s=load_schematic(Path('output.litematic')) a=s.read_flat(0,s.volume).reshape(s.size_yzx) for y in (2,3): for z in (2,3,4,5): print((7,y,z),s.palette[int(a[y,z,7])]) print('Deliverables:') for p in [Path('output.litematic'),Path('generate.py'),Path('test/test_cottage.py'),*sorted(Path('previews').glob('*.png'))]: print(p, p.stat().st_size) PY [23] /opt/venv/bin/python - <<'PY' from pathlib import Path for name in ('interior.litematic', 'loft.litematic'): (Path('previews') / name).unlink() print('Removed temporary cutaway schematics; output.litematic is the sole finished schematic.') PY
red-roof-farmstead — 20 条命令,3 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g '*.jpg' . /work 2>/dev/null | head -70 [01] cat /work/AGENTS.md /work/generator/workspace/AGENTS.md /opt/tools/template_generator.py /opt/tools/template_render.mjs [02] cp /opt/tools/template_generator.py generate.py && cp /opt/tools/template_render.mjs render.mjs && /opt/venv/bin/python - <<'PY' import json from pathlib import Path p=Path('/work/generator/MCRender/resources/1.21.1-blocks.json') d=json.loads(p.read_text()) print(type(d),str(d)[:1500]) PY [03] rg -n 'class .*Litem|regions|block_entities|tile_entities' /work/generator/MCIO/mcio/schematic.py /work/generator/MCIO/mcio -g '*.py' | head -65 [04] sed -n '630,710p' /work/generator/MCIO/mcio/litematica.py && sed -n '65,130p' /work/generator/MCIO/mcio/sketch.py && sed -n '1,100p' /opt/tools/cutaway.py [05] cat > /tmp/farm_methods.py <<'PY' def p(self, x: int, y: int, z: int, block: str, **properties: str) -> None: """Place a short vanilla block name at XYZ, forwarding its properties.""" self.put(x, y, z, 'minecraft:' + block, **properties) def fill(self, bounds: Tuple[int, int, int, int, int, int], block: str, **properties: str) -> None: """Fill inclusive XYZ bounds using a short vanilla block name.""" self.box(bounds, 'minecraft:' + block, **properties) def mix(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Texture inclusive XYZ bounds from short vanilla block names.""" self.texture(bounds, ['minecraft:' + name for name in palette]) @staticmethod def roof_height(z: int) -> int: """Return the stepped main roof height at a north/south coordinate.""" return 9 + min(z - 5, 24 - z) def terrain(self) -> None: """Build the flat square meadow, stone plinth, and branching farm track.""" self.mix((0, 0, 0, 41, 0, 41), ['stone', 'stone', 'andesite', 'cobblestone']) self.fill((0, 1, 0, 41, 1, 41), 'dirt') self.fill((0, 2, 0, 41, 2, 4 ...(截断) [06] chmod u+w generate.py render.mjs /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('"size_xyz": (45, 40, 35)', '"size_xyz": (42, 29, 42)').replace('"name": "build"', '"name": "Red Roof Farmstead"').replace('"description": "Generated build"', '"description": "Timber farmhouse, furnished loft, well, wheat garden and four-sail windmill. Java 1.21.1."') a=s.index(' def build(self)') b=s.index(' def export(self', a) s=s[:a]+Path('/tmp/farm_methods.py').read_text()+s[b:] p.write_text(s) p=Path('render.mjs') s=p.read_text().replace('?? 512','?? 960').replace('[47, 52, 59, 255]','[171, 178, 164, 255]').replace("azimuth: 210, elevation: 18, radius: 3.3", "azimuth: 35, elevation: 28, radius: 3.05").replace("azimuth: 35, elevation: 22, radius: 3.3", "azimuth: 215, elevation: 25, radius: 3.1").replace("azimuth: 120, elevation: 14, radius: 3.3", "azimuth: 305, elevation: 23, radius: 3.1").replace("azimuth: 210, elevation: 62, radius: 3.4", "azimuth: 35, elevation: 60, radius: 3.15") p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py /opt/venv/bin/python generate.py xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-no ...(截断) [07] /opt/venv/bin/python - <<'PY' from generate import Build from mcio.schematic import load_schematic b=Build().build();l=load_schematic('output.litematic');ids=l.read_flat(0,l.volume).reshape(l.size_yzx) n=0 for (x,y,z),s in b.placed.items(): t=l.palette[int(ids[y,z,x])] if s!=t: print((x,y,z),repr(s),repr(t)); n+=1 if n==12:break PY [08] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace('return self.canvas.material(name, **properties)', 'return self.canvas.material(name, **dict(sorted(properties.items())))') s=s.replace("['red_nether_bricks'] * 8 + ['mangrove_planks', 'bricks']", "['mangrove_planks'] * 8 + ['red_nether_bricks', 'red_terracotta']") s=s.replace("'red_nether_brick_stairs' if self.rng.random() < .82 else 'brick_stairs'", "'mangrove_stairs' if self.rng.random() < .90 else 'red_nether_brick_stairs'") s=s.replace("self.fill((16, 17, 12, 21, 19, 17), 'spruce_planks')", "self.fill((16, 17, 12, 21, 18, 17), 'spruce_planks')") s=s.replace("self.fill((16, 20, 12, 21, 21, 17), 'yellow_stained_glass')", "self.fill((16, 19, 12, 21, 20, 17), 'gray_stained_glass')") s=s.replace("self.fill((17, 20, 13, 20, 21, 16), 'glowstone')", "self.fill((17, 19, 13, 20, 20, 16), 'glowstone')") s=s.replace("self.fill((x, 19, z, x, 21, z), 'oak_log', axis='y')", "self.fill((x, 18, z, x, 20, z), 'oak_log', axis='y')") s=s.replace("self.fill((15, 22, 11, 22, 22, 18), 'smooth_sandstone')", "self.fill((15, 21, 11, 22, 21, 18), 'smooth_sandstone')") s=s.replace("self.p(x, 23, z, 's ...(截断) [09] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_floor.litematic --output previews/ground_floor.png --size 960 --azimuth 35 --elevation 60 --radius 3.2 --background 171,178,164,255 [10] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/loft.litematic --output previews/loft.png --size 960 --azimuth 35 --elevation 65 --radius 3.2 --background 171,178,164,255 [11] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [12] # exit 1 mkdir -p test cat > test/test_farmstead.py <<'PY' # Validate the delivered farmstead's geometry, supported details, and reproducibility. from __future__ import annotations from collections import deque import gzip import json from pathlib import Path import subprocess import sys import tempfile import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG from mcio.nbt.nbt import File as NBTFile from mcio.schematic import load_schematic DIRECTIONS = [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)] FACING = {'north': (0, 0, -1), 'south': (0, 0, 1), 'west': (-1, 0, 0), 'east': (1, 0, 0)} PLANTS = {'short_grass', 'tall_grass', 'poppy', 'oxeye_daisy', 'cornflower', 'dandelion', 'wheat'} AIR = {'air', 'cave_air', 'void_air'} def split_state(state): """Split a palette string into its vanilla name and property mapping.""" name, _, tail = state.partition('[') properties = dict(pair.split('=') for pair in tail.rstrip(']').split(',') if pair) return name.removeprefix('minecraft:'), properties class World: """Expose reloaded schematic blocks in XYZ order for independ ...(截断) [13] rg -n 'class File|def load|def parse|def from|def read' /work/generator/MCIO/mcio/nbt/nbt.py | head -30 sed -n '170,240p' /work/generator/MCIO/mcio/schematic.py /opt/venv/bin/python - <<'PY' from test.test_farmstead import * w=World(ROOT/'output.litematic') def leaks(p): n,_=w.at(p) return n in AIR|PLANTS|{'ladder','chain','lantern','candle','flower_pot','iron_bars','campfire'} or n.endswith(('_stairs','_slab','_fence','_carpet','_bed','_fence_gate')) start=(18,5,20);q=deque([start]);prev={start:None};end=None while q: p=q.popleft() if p[1]==28:end=p;break for d in DIRECTIONS: n=add(p,d);x,y,z=n if 0<=x<42 and 0<=y<29 and 0<=z<42 and n not in prev and leaks(n):prev[n]=p;q.append(n) path=[] while end:path.append((end,w.name(end)));end=prev[end] print('LEAK PATH',list(reversed(path))) PY [14] # exit 1 sed -n '220,285p' /work/generator/MCIO/mcio/nbt/nbt.py /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace("for z in range(18, 25):\n self.fill((x, h - 1, z", "for z in range(18, 25):\n if h < self.roof_height(z):\n continue\n self.fill((x, h - 1, z") s=s.replace("self.fill((11, 7, 8, 15, 7, 8),", "self.fill((13, 7, 8, 15, 7, 8),") s=s.replace("self.p(11, 8, 8, 'flower_pot')", "self.p(13, 8, 8, 'flower_pot')") s=s.replace("for y in range(4, 10):\n self.p(10, y, 14", "for y in range(4, 11):\n self.p(10, y, 14") s=s.replace("self.p(23, 11, 12, 'lantern', hanging='false')", "self.p(23, 11, 12, 'lantern', hanging='false')\n self.p(25, 10, 17, 'barrel', facing='up', open='false')\n self.p(25, 11, 17, 'lantern', hanging='false')") s=s.replace("self.p(31, 3, z, 'oak_slab', type='bottom')", "self.fill((30, 3, z, 32, 3, z), 'oak_slab', type='bottom')\n self.p(36, 2, 33, 'water', level='0')\n self.p(36, 3, 33, 'oak_slab', type='bottom')") p.write_text(s) p=Path('test/test_farmstead.py');s=p.read_text().replace('NBTFile.from_buf ...(截断) [15] # exit 1 rg -n 'def save' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt/nbt.py sed -n '960,1015p' /work/generator/MCIO/mcio/litematica.py sed -n '282,320p' /work/generator/MCIO/mcio/nbt/nbt.py /opt/venv/bin/python - <<'PY' from mcio.nbt.nbt import File PY [16] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('import argparse\n','import argparse\nimport gzip\n') s=s.replace("self.fill((10, 10, 11, 10, 11, 12), 'bookshelf')", "self.fill((10, 10, 11, 10, 11, 12), 'bookshelf')\n self.p(10, 12, 11, 'lantern', hanging='false')") s=s.replace(' schematic.save(path)\n return path', ' schematic.save(path)\n # Java Litematica consumes compressed NBT. Pin gzip time as well.\n path.write_bytes(gzip.compress(path.read_bytes(), mtime=0))\n return path') p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test [17] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/ground_floor.litematic --max-y 8 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/loft.litematic --max-y 12 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [18] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_floor.litematic --output previews/ground_floor.png --size 960 --azimuth 35 --elevation 60 --radius 3.6 --background 171,178,164,255 [19] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/loft.litematic --output previews/loft.png --size 960 --azimuth 35 --elevation 60 --radius 3.6 --background 171,178,164,255
stilt-water-cabin — 21 条命令,2 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g '*.jpg' -g '*.md' . [01] cat /work/AGENTS.md && cat /opt/tools/template_generator.py && cat /opt/tools/template_render.mjs [02] cat AGENTS.md && rg -n 'entities|block_entities|tile_entities|class Litematic|class Litematica' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/schematic.py /work/generator/MCIO/mcio -g '*.py' [03] python - <<'PY' import json from pathlib import Path p=Path('/work/generator/MCRender/resources/1.21.1-blocks.json') d=json.loads(p.read_text()) print(type(d),str(d)[:1400]) PY [04] sed -n '60,145p' /work/generator/MCIO/mcio/sketch.py && sed -n '280,410p' /work/generator/MCIO/mcio/sketch.py && sed -n '470,635p' /work/generator/MCIO/mcio/litematica.py && sed -n '636,835p' /work/generator/MCIO/mcio/litematica.py [05] rg -n 'entit|boat' /work/generator/MCRender/src /work/generator/MCIO/mcio/nbt/nbt.py /work/generator/MCIO/mcio/nbt/tag.py | head -80 && cat /opt/tools/cutaway.py [06] cp /opt/tools/template_generator.py generate.py && cp /opt/tools/template_render.mjs render.mjs [07] sed -n '1,155p' /work/generator/MCIO/mcio/nbt/nbt.py && rg -n 'def save|write_to_buffer|gzip' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt/nbt.py && sed -n '1,95p' /work/generator/MCIO/mcio/nbt/tag.py && sed -n '140,175p' /work/generator/MCIO/mcio/sketch.py [08] sed -n '1310,1345p' /work/generator/MCIO/mcio/litematica.py && sed -n '325,360p' /work/generator/MCIO/mcio/nbt/nbt.py [09] cat > /tmp/implement_cabin.py <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('import argparse\n','import argparse\nimport json\n') s=s.replace('from mcio.schematic import load_schematic # noqa: E402','from mcio.schematic import load_schematic # noqa: E402\nfrom mcio.nbt.nbt import File as NBTFile\nfrom mcio.nbt.tag import Byte, Compound, Double, Float, Int, IntArray, List as NBTList, String') s=s.replace('"size_xyz": (45, 40, 35)', '"size_xyz": (28, 21, 24)') s=s.replace('"name": "build"','"name": "Reedwater Cabin"').replace('"description": "Generated build"','"description": "Oak stilt fisherman cabin with spruce deck, dark oak gable, furnished room, shallow pond and oak boat."') s=s.replace('Position = Tuple[int, int, int]','''# Water occupies y=1..2. Floor tops are y=7, landing top y=4. WATER_Y = 2 FLOOR_Y = 6 WALLS = (9, 6, 23, 16) # x0, z0, x1, z1 DOORS = ((9, 7, 10), (9, 7, 11)) BOAT_POSITION = (10.25, 2.65, 21.1) POSTS = ((9, 6), (16, 6), (23, 6), (9, 16), (16, 16), (23, 16), (9, 9), (9, 12), (23, 11)) DECK_POSTS = ((3, 5), (3, 11), (3, 17), (8, 5), (8, 17), (13, 18)) WINDOWS = tuple((x, y, z) for z in (6, 16) for x in (12, 13, 19, 20) for y in ...(截断) [10] # exit 1 chmod u+w generate.py render.mjs && python /tmp/implement_cabin.py && /opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py [11] python - <<'PY' from generate import Build,CONFIG from mcio.schematic import load_schematic b=Build().build() l=load_schematic('output.litematic') i=l.read_flat(0,l.volume).reshape(l.size_yzx) for (x,y,z),s in b.placed.items(): t=l.palette[int(i[y,z,x])] if t!=s: print((x,y,z),s,t); break print(CONFIG) PY sed -n '1,55p' generate.py [12] python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('import json\n','').replace('return self.canvas.material(name, **properties)','return self.canvas.material(name, **dict(sorted(properties.items())))') s=s.replace(' return {\n "size_xyz"', ''' root = NBTFile.load_regardless_of_gzipped(path) region = root["Regions"][CONFIG["name"]] assert list(region["Entities"]) == self.entities, "entity data changed on reload" assert list(region["TileEntities"]) == self.block_entities, "block entity data changed on reload" return { "size_xyz"''') p.write_text(s) p=Path('render.mjs');s=p.read_text().replace("{ name: 'front', azimuth: 210, elevation: 18, radius: 3.3 }", "{ name: 'front', azimuth: 225, elevation: 22, radius: 2.5 }").replace("{ name: 'rear', azimuth: 35, elevation: 22, radius: 3.3 }", "{ name: 'rear', azimuth: 45, elevation: 24, radius: 2.5 }").replace("{ name: 'side', azimuth: 120, elevation: 14, radius: 3.3 }", "{ name: 'side', azimuth: 305, elevation: 15, radius: 2.5 }").replace("{ name: 'top', azimuth: 210, elevation: 62, radius: 3.4 }", "{ name: 'top', azimuth: 225, elevation: ...(截断) [13] python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('self.box((x, 16, 11, x, 18, 11), "minecraft:stripped_spruce_log", axis="y")','self.box((x, 16, 11, x, 17, 11), "minecraft:spruce_planks")').replace('self.put(x, 19, 11, "minecraft:spruce_slab"','self.put(x, 18, 11, "minecraft:spruce_slab"') s=s.replace('# Starting point for a Minecraft build generator. Copy into the workspace and fill in build().\n# Everything that has to be right for the harness is already right here: bounds checked placement,\n# one seeded random source, and a byte reproducible export.','# Generate Reedwater Cabin and validate a deterministic single-region Java 1.21.1 export.') p.write_text(s) p=Path('render.mjs');s=p.read_text();a=s.index('const VIEWS = [');b=s.index('];',a)+2;s=s[:a]+'''const VIEWS = [ { name: 'front', azimuth: 305, elevation: 20, radius: 3.0 }, { name: 'rear', azimuth: 125, elevation: 24, radius: 3.0 }, { name: 'side', azimuth: 225, elevation: 16, radius: 3.0 }, { name: 'top', azimuth: 305, elevation: 62, radius: 3.1 }, ];'''+s[b:];p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py && /opt/venv/bin/pyt ...(截断) [14] python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('((10, 7, 7), (11, 7, 7), (10, 8, 7), (22, 7, 8))', '((10, 7, 12), (10, 7, 13), (10, 8, 12), (22, 7, 9))').replace('self.put(22, 8, 8,','self.put(22, 8, 9,').replace('((8, "head"), (9, "foot"))','((9, "head"), (10, "foot"))');p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py && /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/interior.litematic --max-y 9 && RENDER_SIZE=900 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [15] # exit 1 mkdir -p test cat > test/test_cabin.py <<'PY' # Verify the exported cabin geometry, access, attachments, entity data, and determinism. from __future__ import annotations from collections import deque import json from pathlib import Path import shutil import subprocess import sys import tempfile import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import BOAT_POSITION, CONFIG, DECK_POSTS, DOORS, FLOOR_Y, LIGHTS, POSTS, WINDOWS, Build from mcio.nbt.nbt import File as NBTFile from mcio.schematic import load_schematic DIRECTIONS = ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)) HORIZONTAL = ((1, 0), (-1, 0), (0, 1), (0, -1)) FACING = {"east": (1, 0), "west": (-1, 0), "south": (0, 1), "north": (0, -1)} AIR = "minecraft:air" def parse(state): """Split a canonical block state into its name and properties. Args: state (str): Namespaced state string. Returns: tuple: Name and property mapping. """ name, _, props = state.partition("[") return name, dict(p.split("=") for p in props.rstrip("]").split(",") if p) class SavedBuild: """Read geometry and entities independently ...(截断) [16] python - <<'PY' from pathlib import Path p=Path('test/test_cabin.py');s=p.read_text().replace('assert saved.name(x, 5, z) == "minecraft:spruce_log"','assert saved.name(x, 5, z) == ("minecraft:oak_log" if (x, z) in POSTS else "minecraft:spruce_log")');p.write_text(s) PY /opt/venv/bin/python -m yapf -i test/test_cabin.py && /opt/venv/bin/python -m pytest -q test && xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/interior.litematic --output previews/interior.png --size 1000 --azimuth 305 --elevation 60 --radius 2.8 [17] python - <<'PY' from pathlib import Path import sys sys.path.insert(0, str(Path('test').resolve())) from test_cabin import SavedBuild,walkable,reachable s=SavedBuild(Path('output.litematic'));n=walkable(s);r=reachable(n,(5,4,22));print('Unreachable supported foot cells:', sorted(n-r)) PY [18] python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('(13, 18))','(14, 19))') s=s.replace('self.box((9, 6, 17, 13, 6, 18), "minecraft:spruce_planks")','self.box((8, 6, 17, 14, 6, 19), "minecraft:spruce_planks")\n self.box((8, 5, 19, 14, 5, 19), "minecraft:spruce_log", axis="x")') s=s.replace(''' self.box((x, 0, z, x, 7, z), "minecraft:oak_log", axis="y") self.put(x, 8, z, "minecraft:spruce_slab", type="bottom", waterlogged="false")''',''' top = 6 if (x, z) == (8, 17) else 7 self.box((x, 0, z, x, top, z), "minecraft:oak_log", axis="y") if top == 7: self.put(x, 8, z, "minecraft:spruce_slab", type="bottom", waterlogged="false")''') s=s.replace('''| {(x, 17) for x in (7, 8)} | {(x, 18) for x in range(9, 14)} | {(13, 17)}''','''| {(7, 17)} | {(x, 19) for x in range(8, 15)} | {(14, 17), (14, 18)}''') p.write_text(s) p=Path('test/test_cabin.py');s=p.read_text().replace(' assert set(DOORS) <= reached',' assert nodes == reached, f"unreachable supported floors: {sorted(nodes - reached)}"\n assert set(DOORS) <= reached');p.write_text(s) p=Path('render.mjs');s=p.read_text ...(截断) [19] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/interior.litematic --max-y 9 && RENDER_SIZE=1000 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews && xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/interior.litematic --output previews/interior.png --size 1000 --azimuth 305 --elevation 60 --radius 3.1 [20] cat > README.md <<'EOF' # Reedwater Cabin Furnished Java 1.21.1 stilt cabin, exported as one region in `output.litematic`. The schematic is 28 x 24 blocks horizontally, including the pond and banks. The cabin floor and entrance deck share Y=6, above two layers of shallow water. A wider return deck keeps the route around the window sills open. The room contains a complete green bed, smoker, crafting counter, water basin, stocked fishing barrels, window table, chairs, rug, and four hanging lanterns. Two more lanterns hang at the ends of the waterside eave. An oak boat entity floats next to the landing and mooring bollard. Enable entity placement when pasting to include the boat. The block-only preview renderer omits the boat. Run from any clean working directory with the supplied environment: ```sh /opt/venv/bin/python /work/generator/workspace/generate.py ``` The no-argument generator writes `output.litematic` to the current directory, then reloads and compares every block, container inventory, and entity. All random choices use the configured seed. Metadata timestamps and the boat UUID are stable, and the tests verify a byte-identical rerun in a clean directory. Run the struc ...(截断)
stilted-manor — 28 条命令,3 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g '!node_modules' . /work | head -60 [01] cat /work/AGENTS.md && cat AGENTS.md && cat /opt/tools/template_generator.py && cat /opt/tools/template_render.mjs [02] cp /opt/tools/template_generator.py generate.py && cp /opt/tools/template_render.mjs render.mjs && /opt/venv/bin/python - <<'PY' import json from pathlib import Path p=Path('/work/generator/MCRender/resources/1.21.1-blocks.json') d=json.loads(p.read_text()) print(type(d), str(d)[:1800]) PY [03] cat > /tmp/manor_methods.txt <<'PY' def timber(self, bounds, axis='y', dark=False): """Place structural timber with its grain along the member.""" self.box(bounds, 'minecraft:dark_oak_log' if dark else 'minecraft:stripped_spruce_log', axis=axis) def stairs(self, x, y, z, facing, block='spruce', half='bottom'): """Place a straight stair with explicit stable state.""" self.put(x, y, z, f'minecraft:{block}_stairs', facing=facing, half=half, shape='straight', waterlogged='false') def slab(self, x, y, z, block='spruce', kind='bottom'): """Place a slab with an explicit half.""" self.put(x, y, z, f'minecraft:{block}_slab', type=kind, waterlogged='false') def door(self, x, y, z, facing='north', hinge='left'): """Place both matching halves of an operable door.""" for half, dy in [('lower', 0), ('upper', 1)]: self.put(x, y + dy, z, 'minecraft:spruce_door', facing=facing, hinge=hinge, half=half, open='false', powered='false') def trap(self, x, y, z, facing, half='bottom'): """Attach a vertical spruce shutter or planter facing.""" self.put(x, y, z, 'minecraft:spruce_trapdoor', fa ...(截断) [04] chmod u+w generate.py render.mjs /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('"size_xyz": (45, 40, 35)', '"size_xyz": (33, 39, 31)').replace('"name": "build"', '"name": "The Brackenwatch Manor"').replace('"description": "Generated build"', '"description": "Stilted medieval manor with workshop, great room, two bedchambers, and ridge lookout. Java 1.21.1."') a=s.index(' def build(self)');b=s.index(' def export(',a) s=s[:a]+Path('/tmp/manor_methods.txt').read_text()+s[b:] i=s.index('Position = ') s=s[:i]+'''# Restrained weighted palettes keep masonry and plaster readable at this scale. STONE = ['minecraft:stone_bricks'] * 6 + ['minecraft:cobblestone'] * 3 + ['minecraft:andesite'] BASE = STONE + ['minecraft:mossy_stone_bricks'] * 3 + ['minecraft:mossy_cobblestone'] * 2 PLASTER = ['minecraft:smooth_sandstone'] * 10 + ['minecraft:cut_sandstone'] * 2 + ['minecraft:sandstone'] FLOOR = ['minecraft:spruce_planks'] * 9 + ['minecraft:dark_oak_planks'] ROOF_SOLID = ['minecraft:stone_bricks'] * 6 + ['minecraft:cobblestone'] * 2 + ['minecraft:andesite'] '''+s[i:] p.write_text(s) p=Path('render.mjs');s=p.read_text().replace('?? 512', ...(截断) [05] /opt/venv/bin/python - <<'PY' from pathlib import Path from generate import Build,load_schematic b=Build().build();s=load_schematic(Path('output.litematic'));ids=s.read_flat(0,s.volume).reshape(s.size_yzx) for p in [(19,3,9),(20,3,9),(17,38,15)]: x,y,z=p print(p,b.get(*p),s.palette[int(ids[y,z,x])]) print('differing',sum(s.palette[int(ids[y,z,x])]!=state for (x,y,z),state in b.placed.items())) PY [06] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('return self.canvas.material(name, **properties)', 'return self.canvas.material(name, **dict(sorted(properties.items())))') s=s.replace('self.texture((25, 10, 12, 26, 30, 13)', 'self.texture((24, 10, 12, 26, 30, 13)') s=s.replace('for x in [25, 26]:\n for z in [12, 13]:', 'for x in [24, 25, 26]:\n for z in [12, 13]:') s=s.replace('[(25, 12), (26, 13)]', '[(24, 12), (26, 12)]') a=s.index(' def cupola(');b=s.index(' def chimney(',a) s=s[:a]+''' def cupola(self): """Crown the ridge with a compact glazed lookout and pointed finial.""" self.box((15, 27, 13, 19, 31, 17), 'minecraft:air') self.texture((14, 27, 12, 20, 27, 18), FLOOR) for x in range(15, 20): for z in range(13, 18): if x in [15, 19] or z in [13, 17]: for y in range(28, 31): self.put(x, y, z, 'minecraft:glass') self.put(x, 31, z, 'minecraft:dark_oak_log', axis='x' if z in [13, 17] else 'z') for x in [15, 17, 19]: for z in [13, 17]: self.t ...(截断) [07] rg -n 'class .*Litematic|def load|regions|minecraft_data_version' /work/generator/MCIO/mcio/schematic.py /work/generator/MCIO/mcio/formats/litematic.py /work/generator/MCIO/mcio/nbt/nbt.py /work/generator/MCIO/mcio/schematic -g '*.py' | head -65 [08] mkdir -p test /opt/venv/bin/python - <<'PY' from pathlib import Path from generate import load_schematic s=load_schematic(Path('output.litematic')) print(type(s));print(s.__dict__.keys()) PY [09] # exit 1 cat > test/test_structure.py <<'PY' # Validate the exported manor's geometry, circulation, state pairs, and reproducibility. from collections import deque import hashlib import json from pathlib import Path import sys import tempfile import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG, load_schematic from mcio.nbt.nbt import File as NBTFile DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)} ROOMS = { 'entrance': (19, 3, 11), 'storage': (17, 3, 20), 'workshop': (21, 3, 18), 'great room': (17, 10, 18), 'hearth': (22, 10, 12), 'west bedroom': (11, 17, 15), 'east bedroom': (21, 17, 17), 'cupola lookout': (17, 28, 15), } def split_state(state): """Read a palette block name and its property mapping.""" name, _, rest = state.partition('[') return name.removeprefix('minecraft:'), dict(item.split('=') for item in rest.rstrip(']').split(',') if item) class Volume: """Expose the reloaded schematic in XYZ coordinates for spatial checks.""" def __init__(self, path): self.loaded = load_schematic(path) s ...(截断) [10] /opt/venv/bin/python - <<'PY' import sys sys.path.insert(0,'test') from test_structure import Volume,ROOT from collections import deque v=Volume(ROOT/'output.litematic') start=(17,12,18);q=deque([start]);prev={start:None};end=None while q: p=q.popleft();x,y,z=p if x in [0,v.sx-1] or y==v.sy-1 or z in [0,v.sz-1]: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)]: t=x+dx,y+dy,z+dz if v.inside(t) and t not in prev and not v.full(t):prev[t]=p;q.append(t) p=end;path=[] while p is not None:path.append((p,v.at(p)[0]));p=prev[p] print(path[::-1]) PY [11] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace("for z in range(6, 12):\n self.put(x, top - 1, z", "for z in range(6, 12):\n if top < 17 + min(z - 4, 26 - z):\n continue\n self.put(x, top - 1, z") s=s.replace("self.texture((14, 27, 12, 20, 27, 18), FLOOR)", "self.texture((14, 27, 12, 20, 27, 18), FLOOR)\n for x in range(15, 20):\n for z in range(13, 18):\n if x in [15, 19] or z in [13, 17]:\n self.timber((x, 25, z, x, 26, z))") s=s.replace("self.box((20, 10, 23, 24, 12, 23), 'minecraft:bookshelf')", "self.box((20, 10, 23, 24, 10, 23), 'minecraft:bookshelf')\n self.box((19, 10, 21, 19, 12, 22), 'minecraft:bookshelf')") s=s.replace("[(10, 11), (17, 17), (21, 16), (15, 20)]", "[(10, 11), (17, 17), (21, 16), (15, 20), (19, 11)]") s=s.replace("for z in [6, 15, 24]:\n self.timber", "for x, z in posts:\n for dx, dz, face in [(1, 0, 'west'), (-1, 0, 'east'), (0, 1, 'north'), (0, -1, 'south')]:\n nx, nz = x + 2 * dx, z + 2 * dz\n if 5 <= nx <= 27 and 6 <= nz <= 2 ...(截断) [12] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/ground_cutaway.litematic --max-y 8 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/living_cutaway.litematic --max-y 15 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/attic_cutaway.litematic --max-y 20 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [13] /opt/venv/bin/python - <<'PY' import sys sys.path.insert(0,'test') from test_structure import Volume,ROOT from collections import deque v=Volume(ROOT/'output.litematic') for start in [(17,12,18),(21,19,17),(17,29,15)]: q=deque([start]);prev={start:None};end=None while q: p=q.popleft();x,y,z=p if x in [0,v.sx-1] or y==v.sy-1 or z in [0,v.sz-1]: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)]: t=x+dx,y+dy,z+dz if v.inside(t) and t not in prev and not v.full(t):prev[t]=p;q.append(t) p=end;path=[] while p is not None:path.append((p,v.at(p)[0]));p=prev[p] print(start,path[::-1]) PY [14] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace("if 5 <= nx <= 27 and 6 <= nz <= 24:\n self.stairs(nx, 7, nz, face, half='top')", "if 5 <= nx <= 27 and 6 <= nz <= 24 and not (8 <= nx <= 24 and 9 <= nz <= 22):\n self.stairs(nx, 7, nz, face, half='top')");p.write_text(s) PY /opt/venv/bin/python -m yapf -i generate.py /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_structure.py [15] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/ground_cutaway.litematic --max-y 8 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_cutaway.litematic --output previews/ground_interior.png --size 768 --azimuth 210 --elevation 55 --radius 3.1 --background 226,224,210,255 [16] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/attic_cutaway.litematic --max-y 20 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/attic_cutaway.litematic --output previews/attic_interior.png --size 768 --azimuth 210 --elevation 65 --radius 3.1 --background 226,224,210,255 [17] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/living_cutaway.litematic --max-y 15 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/living_cutaway.litematic --output previews/living_interior.png --size 768 --azimuth 210 --elevation 60 --radius 3.1 --background 226,224,210,255 [18] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input output.litematic --output previews/hero.png --size 1024 --azimuth 150 --elevation 13 --radius 3.3 --background 226,224,210,255 [19] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace("[(9, 11), (16, 18), (21, 22)]", "[(8, 10), (16, 18), (21, 22)]") s=s.replace("['stone_brick'] * 5 + ['cobblestone'] * 2 + ['mossy_stone_brick']", "['stone_brick'] * 12 + ['cobblestone'] * 4 + ['mossy_stone_brick']") s=s.replace("self.put(x, 10, 13, 'minecraft:spruce_fence', east='true', west='true', north='false', south='false', waterlogged='false')\n self.slab(x, 11, 13, 'dark_oak', 'top')", "if x in [14, 18]:\n self.stairs(x, 10, 13, 'east' if x == 14 else 'west', 'dark_oak', 'top')\n else:\n self.slab(x, 10, 13, 'dark_oak', 'top')") s=s.replace("self.candle(15, 12, 13, 3)", "self.candle(15, 11, 13, 3)").replace("self.put(17, 12, 13, 'minecraft:potted_red_tulip')", "self.put(17, 11, 13, 'minecraft:potted_red_tulip')") s=s.replace("self.box((6, 17, 14, 6, 19, 17)", "self.box((6, 17, 14, 6, 19, 16)").replace("self.box((26, 17, 17, 26, 19, 19)", "self.box((26, 17, 14, 26, 19, 16)") s=s.replace("self.put(26, 17, 15, 'minecraft:barrel'", "self.put(26, 17, 20, 'minecraft:barrel'").replace("self.candle(26, 18, 15)", "self.candle(26, 18 ...(截断) [20] rg -n 'const VIEWS|name:|SIZE|input|output' render.mjs /opt/tools/render_schematic.mjs | head -35 rg -n 'class Litematic|def save|gzip' /work/generator/MCIO/mcio -g '*.py' | head -35 [21] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text();a=s.index(' def chimney(');b=s.index(' def furnishings(',a);part=s[a:b] part=part.replace('(24, 10, 12, 26, 30, 13)', '(23, 10, 12, 25, 30, 13)').replace('for x in [24, 25, 26]', 'for x in [23, 24, 25]').replace('[(24, 12), (26, 12)]', '[(23, 12), (25, 12)]') part=part.replace('self.put(24, 10, z,', 'self.put(22, 10, z,').replace('(24, 10, z, 26, 12, z)', '(22, 10, z, 25, 12, z)').replace('(24, 13, 11, 26, 13, 14)', '(22, 13, 11, 25, 13, 14)').replace('self.lantern(24, 14, 11)', 'self.lantern(22, 14, 11)') s=s[:a]+part+s[b:];p.write_text(s) p=Path('test/test_structure.py');s=p.read_text().replace("'hearth': (22, 10, 12)", "'hearth': (21, 10, 12)").replace('for x in [24, 26]', 'for x in [23, 25]').replace('import sys\n', 'import sys\nimport subprocess\n') s=s.replace("a, b = Path(directory) / 'a.litematic', Path(directory) / 'b.litematic'", "a, b = Path(directory) / 'a.litematic', Path(directory) / 'output.litematic'") s=s.replace("second = Build().build()\n second.export(b)\n second.verify(b)", "subprocess.run([sys.executable, str(ROOT / 'generate.py')], cwd=directory, c ...(截断) [22] cat > README.md <<'MD' # The Brackenwatch Manor A furnished Minecraft Java 1.21.1 manor, reconstructed from the supplied reference. The single region is 33 blocks wide, 31 deep, and 39 high including the finial. The house has a recessed stone workshop and storage floor, an overhanging timber great room, two attic bedrooms, and a ladder-accessible ridge lookout. Two wide staircases connect the main floors. - `output.litematic`: finished schematic; origin is the northwest bottom corner. - `generate.py`: seeded, self-contained generator using the installed MCIO library. - `test/test_structure.py`: tests the exported blocks, including navigation, roof sealing, windows, decoration supports, beds, doors, lighting, and deterministic regeneration. - `render.mjs`: five exterior camera views. - `previews/`: exterior renders and cutaway renders of the three furnished floors. The entrance faces north (negative Z). Its double doors occupy X=19..20, Y=3..4, Z=9. Ground-floor standing height is Y=3, the great room is Y=10, the bedrooms are Y=17, and the lookout is Y=28. The cupola ladder is at X=18, Z=16. Run in this environment: ```sh /opt/venv/bin/python generate.py /opt/venv/bin/python -m ...(截断) [23] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text();lines=s.splitlines();lines[:3]=['# Generate and verify a furnished stilted medieval manor for Minecraft Java 1.21.1.'];p.write_text('\n'.join(lines)+'\n') p=Path('render.mjs');s=p.read_text().replace('// Starting point for rendering a build. Copy into the workspace and edit VIEWS.', '// Render five exterior views of the finished stilted manor.');p.write_text(s) PY [24] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/ground_cutaway.litematic --max-y 8 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_cutaway.litematic --output previews/ground_interior.png --size 1024 --azimuth 210 --elevation 55 --radius 3.1 --background 226,224,210,255 [25] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/living_cutaway.litematic --max-y 15 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/living_cutaway.litematic --output previews/living_interior.png --size 1024 --azimuth 210 --elevation 60 --radius 3.1 --background 226,224,210,255 [26] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/attic_cutaway.litematic --max-y 20 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/attic_cutaway.litematic --output previews/attic_interior.png --size 1024 --azimuth 210 --elevation 65 --radius 3.1 --background 226,224,210,255 [27] /opt/venv/bin/python - <<'PY' from pathlib import Path import shutil for name in ['ground', 'living', 'attic']: (Path('previews') / f'{name}_cutaway.litematic').unlink() for directory in [Path('__pycache__'), Path('test/__pycache__'), Path('.pytest_cache')]: if directory.exists(): shutil.rmtree(directory) for path in [Path('output.litematic'), Path('generate.py'), Path('test/test_structure.py'), *sorted(Path('previews').glob('*.png'))]: print(f'{path}: {path.stat().st_size:,} bytes') PY
merchant-manor — 31 条命令,5 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g '*.jpg' . /work 2>/dev/null | head -80 [01] cat /work/AGENTS.md /work/generator/workspace/AGENTS.md /opt/tools/template_generator.py /opt/tools/template_render.mjs [02] cp /opt/tools/template_generator.py generate.py cp /opt/tools/template_render.mjs render.mjs rg -n 'entities|tile_entities|block_entities|to_litematica' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio -g '*.py' | head -90 [03] /opt/venv/bin/python - <<'PY' import json from pathlib import Path p=Path('/work/generator/MCRender/resources/1.21.1-blocks.json') v=json.loads(p.read_text()) print(type(v), list(v)[:5]) print(str(v)[:1300]) PY [04] sed -n '1,245p' /work/generator/MCIO/mcio/sketch.py sed -n '1,260p' /work/generator/MCIO/mcio/litematica.py sed -n '1,230p' /work/generator/MCIO/mcio/nbt/tag.py [05] rg -n 'class LitematicaSchematic|def save|def to_nbt|Entities|TileEntities|def load' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt/nbt.py | tail -45 sed -n '800,1080p' /work/generator/MCIO/mcio/litematica.py [06] sed -n '1300,1360p' /work/generator/MCIO/mcio/litematica.py sed -n '245,335p' /work/generator/MCIO/mcio/nbt/nbt.py sed -n '1,110p' /work/generator/MCRender/src/headless/index.ts [07] cat > /tmp/manor_methods.txt <<'PY' def p(self, x, y, z, name, **properties): """Place a vanilla block using a short name. Args: x, y, z (int): Block coordinates. name (str): Vanilla block name. **properties (str): State properties. """ self.put(x, y, z, 'minecraft:' + name, **properties) def fill(self, bounds, name, **properties): """Fill a box with a short vanilla name. Args: bounds (tuple): Inclusive XYZ bounds. name (str): Vanilla block name. **properties (str): State properties. """ self.box(bounds, 'minecraft:' + name, **properties) def mix(self, bounds, palette): """Texture a box with short vanilla names. Args: bounds (tuple): Inclusive XYZ bounds. palette (list): Weighted names. """ self.texture(bounds, ['minecraft:' + name for name in palette]) def door(self, x, y, z, facing='north', hinge='left'): """Place both matching halves of a door. Args: x, y, z (int): Lower block coordinates. facing, hinge (str): Door state. ...(截断) [08] chmod u+w generate.py render.mjs /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text() s=s.replace('from mcio.schematic import load_schematic', 'from mcio.nbt.nbt import File as NBTFile\nfrom mcio.nbt.tag import Compound, String, Int, Byte, Float, Double, IntArray, List as NBTList\nfrom mcio.schematic import load_schematic') s=s.replace('"size_xyz": (45, 40, 35)', '"size_xyz": (37, 43, 33)') s=s.replace('"name": "build"', '"name": "Merchant Manor"').replace('"description": "Generated build"', '"description": "The Rowan Exchange: timber merchant manor, furnished hall, cooperage, market, bedrooms, library and lookout."') s=s.replace('Position = Tuple[int, int, int]', 'STONE = ["cobblestone"] * 4 + ["stone_bricks"] * 5 + ["andesite"] * 2 + ["mossy_cobblestone"]\nPLASTER = ["smooth_sandstone"] * 9 + ["sandstone"] * 2 + ["cut_sandstone"]\nROOF = ["cobbled_deepslate"] * 5 + ["deepslate_bricks"] * 4 + ["deepslate_tiles"] * 2\n\nPosition = Tuple[int, int, int]') a=s.index(' def build(self)') b=s.index(' def export(', a) s=s[:a]+Path('/tmp/manor_methods.txt').read_text()+'\n'+s[b:] s=s.replace(' schematic.save(path)', ' nbt = schema ...(截断) [09] /opt/venv/bin/python - <<'PY' from generate import Build,load_schematic from pathlib import Path b=Build().build(); s=load_schematic(Path('output.litematic')); ids=s.read_flat(0,s.volume).reshape(s.size_yzx) p=(3,2,14); x,y,z=p print(b.get(*p),s.palette[int(ids[y,z,x])]) PY [10] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text();s=s.replace('self.canvas.material(name, **properties)', 'self.canvas.material(name, **dict(sorted(properties.items())))') s=s.replace(" self.p(x, 4, z, 'red_carpet' if color == 'red_wool' else 'white_carpet') if False else None\n", " pass\n") s=s.replace(" if z == 6:\n pass\n", '') p.write_text(s) PY /opt/venv/bin/python generate.py mkdir -p test /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/hall_slice.litematic --max-y 14 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/upper_slice.litematic --max-y 20 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/storage_slice.litematic --max-y 6 [11] # exit 1 cat > test/test_manor.py <<'PY' # Validate the exported manor's structure, circulation, envelope, furnishings and deterministic NBT. from __future__ import annotations from collections import deque from pathlib import Path import hashlib import json import shutil import sys import tempfile import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG, NBTFile, load_schematic DIRECTIONS = {'east': (1, 0, 0), 'west': (-1, 0, 0), 'south': (0, 0, 1), 'north': (0, 0, -1)} NEIGHBORS = list(DIRECTIONS.values()) + [(0, 1, 0), (0, -1, 0)] ROOMS = { 'cooperage': (6, 2, 12, 20, 7, 27), 'market stockroom': (22, 2, 12, 30, 7, 27), 'hall and kitchen': (8, 9, 14, 28, 14, 25), 'west bedroom': (8, 16, 14, 14, 19, 18), 'east bedroom': (24, 16, 14, 28, 20, 24), 'map room': (16, 16, 18, 22, 21, 24), 'study': (16, 23, 18, 22, 27, 24), 'lookout': (16, 29, 18, 22, 32, 24), 'turret lower landing': (8, 16, 20, 10, 22, 22), 'turret lookout': (8, 24, 20, 10, 28, 22), } TARGETS = { 'cooperage': (15, 2, 19), 'market stockroom': (25, 2, 20), 'hall and kitchen': (18, 9, 17), 'west bedroom': (11, 16, 17 ...(截断) [12] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('19, max(10, y + 3), z','19, y + 3, z').replace('10, max(17, y + 3), z','10, y + 3, z').replace("self.fill((8, 15, 18, 10, 15, 19), 'spruce_planks')", "self.fill((8, 15, 18, 8, 15, 19), 'spruce_planks')").replace('self.door(9, 16, 19', 'self.door(8, 16, 19').replace("self.fill((9, 16, 18, 10, 18, 18), 'air')", "self.fill((8, 16, 18, 8, 18, 18), 'air')") s=s.replace(" self.frame(14, 3, 27, 'wheat')", " self.frame(14, 3, 27, 'wheat')\n self.p(17, 2, 12, 'stone_bricks')\n self.light(17, 3, 12)\n self.p(8, 16, 21, 'spruce_planks')\n self.light(8, 17, 21)") p.write_text(s) p=Path('test/test_manor.py');s=p.read_text().replace("frame['id'] == 'minecraft:item_frame' and frame['Item']['count'] == 1", "str(frame['id']) == 'minecraft:item_frame' and int(frame['Item']['count']) == 1").replace("nbt['Metadata']['TimeCreated'] == nbt['Metadata']['TimeModified'] == 0", "int(nbt['Metadata']['TimeCreated']) == int(nbt['Metadata']['TimeModified']) == 0") p.write_text(s) PY /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_manor.py [13] /opt/venv/bin/python - <<'PY' import sys from pathlib import Path sys.path.insert(0,'test') from test_manor import Volume, Build, TARGETS v=Volume(Path('output.litematic')); b=Build().build(); r=v.reachable((17,9,13)) print('STAIRS') for z in range(16,24): y=32-z+1 print((9,y,z),'node',v.node((9,y,z)),'reached',(9,y,z) in r,[(h,v.at((9,h,z))[0]) for h in range(y-1,y+3)]) print('WINDOWS') for x,y,z,dx,dz in b.windows: if not v.at((x,y,z))[0].endswith('_glass') or v.at((x+dx,y,z+dz))[0] not in ('air','vine'): print((x,y,z),v.at((x,y,z)),v.at((x+dx,y,z+dz))) PY [14] /opt/venv/bin/python - <<'PY' import sys from pathlib import Path sys.path.insert(0,'test') from test_manor import Volume v=Volume(Path('output.litematic'));r=v.reachable((17,9,13)) for y in (15,16,17,18): print('Y',y) for z in range(14,25): print(z,' '.join(('*' if (x,y,z) in r else 'o' if v.node((x,y,z)) else '.')+v.at((x,y,z))[0][:3] for x in range(7,15))) PY [15] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace("self.window(7, 18, 18, 2, 3, 'west')", "self.window(7, 18, 17, 1, 3, 'west')").replace(" self.window(15, 17, 26, 2, 2, 'south')\n", '') s=s.replace('self.window(x, y, 19, 1, height, side)', 'self.window(x, 26 if y == 24 else y, 19, 1, 2 if y == 24 else height, side)') s=s.replace("self.fill((8, 15, 18, 8, 15, 19), 'spruce_planks')", "self.fill((8, 15, 18, 8, 15, 19), 'spruce_planks')\n self.fill((9, 15, 20, 10, 15, 20), 'spruce_planks')") s=s.replace(" self.light(17, 3, 12)", " self.light(17, 3, 12)\n self.p(20, 2, 17, 'stone_bricks')\n self.light(20, 3, 17)") p.write_text(s) PY /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_manor.py [16] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/hall_slice.litematic --max-y 14 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/upper_slice.litematic --max-y 20 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/storage_slice.litematic --max-y 6 [17] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input output.litematic --output previews/approach.png --size 1000 --azimuth 150 --elevation 12 --radius 3.1 [18] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace(" self.fill((9, 15, 20, 10, 15, 20), 'spruce_planks')\n", '').replace("self.door(8, 16, 19, facing='north')", "self.door(11, 16, 21, facing='east')").replace("self.door(8, 16, 19)", "self.door(11, 16, 21, facing='east')") p.write_text(s) PY /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_manor.py [19] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/hall_slice.litematic --output previews/hall.png --size 950 --azimuth 210 --elevation 60 --radius 2.7 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/upper_slice.litematic --output previews/bedrooms.png --size 950 --azimuth 210 --elevation 65 --radius 2.7 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/storage_slice.litematic --output previews/undercroft.png --size 950 --azimuth 210 --elevation 68 --radius 2.7 [20] # exit 1 /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace(" self.p(x, 9, 24, 'spruce_stairs', facing='south')", " if x == 12:\n self.p(x, 9, 24, 'spruce_stairs', facing='south')") s=s.replace("self.fill((21, 9, 25, 25, 9, 25), 'spruce_planks')", "self.fill((21, 9, 25, 24, 9, 25), 'spruce_planks')").replace('self.light(25, 10, 25)', 'self.light(24, 10, 25)') p.write_text(s) PY /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_manor.py [21] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace("self.p(17, 23, 24, 'spruce_planks')", "self.p(16, 23, 24, 'spruce_planks')").replace('self.light(17, 24, 24)', 'self.light(16, 24, 24)').replace("self.p(17, 29, 24, 'spruce_planks')", "self.p(16, 29, 24, 'spruce_planks')").replace('self.light(17, 30, 24)', 'self.light(16, 30, 24)').replace("self.p(8, 16, 21, 'spruce_planks')", "self.p(8, 16, 20, 'spruce_planks')").replace('self.light(8, 17, 21)', 'self.light(8, 17, 20)') p.write_text(s) PY /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_manor.py [22] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() needle=" def build(self) -> 'Build':" method=''' def orient_front(self): """Reflect the plan so the north-facing facade matches the reference's left and right. The architectural plan uses eastward coordinates for the market wing. Seen from the street, west is on the viewer's right. Reflect geometry, states, block entities and hanging entities together before export. """ placed = self.placed self.placed = {} self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) swap = {'east': 'west', 'west': 'east'} for (x, y, z), state in placed.items(): name, _, raw = state.partition('[') props = dict(part.split('=') for part in raw.rstrip(']').split(',') if '=' in part) mirrored = {} for key, value in props.items(): if key == 'facing': value = swap.get(value, value) elif key == 'hinge': value = 'right' if value == 'left' else 'left' elif key == 'shape': ...(截断) [23] /opt/venv/bin/python /opt/tools/cutaway.py --help rg -n 'def orient_front|def build|def test_|front_position|size_xyz' generate.py test/test_manor.py [24] /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('test/test_manor.py');s=p.read_text().replace('import hashlib\n','').replace('import sys\n', 'import sys\nimport subprocess\n') s=s.replace('ROOMS = {name: (36 - x1, y0, z0, 36 - x0, y1, z1)', "ROOMS = {name: (CONFIG['size_xyz'][0] - 1 - x1, y0, z0, CONFIG['size_xyz'][0] - 1 - x0, y1, z1)") s=s.replace(" second = Build().build().export(temporary / 'second.litematic')", " subprocess.run([sys.executable, str(ROOT / 'generate.py')], cwd=temporary, check=True, capture_output=True, text=True)\n second = temporary / 'output.litematic'") p.write_text(s) PY /opt/venv/bin/python -m yapf -i --style /work/.style.yapf test/test_manor.py /opt/venv/bin/python -m pytest -q test/test_manor.py /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/hall_slice.litematic --max-y 14 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/upper_slice.litematic --max-y 18 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/storage_slice.litematic --max-y 6 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --outp ...(截断) [25] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/storage_slice.litematic --output previews/undercroft.png --size 950 --azimuth 210 --elevation 68 --radius 3.0 [26] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/hall_slice.litematic --output previews/hall.png --size 950 --azimuth 210 --elevation 60 --radius 3.0 [27] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/upper_slice.litematic --output previews/bedrooms.png --size 950 --azimuth 210 --elevation 65 --radius 3.0 [28] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/study_slice.litematic --output previews/study.png --size 950 --azimuth 210 --elevation 65 --radius 3.0 [29] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input output.litematic --output previews/approach.png --size 1000 --azimuth 150 --elevation 12 --radius 3.1 [30] /opt/venv/bin/python - <<'PY' from pathlib import Path from generate import load_schematic p=Path('output.litematic'); s=load_schematic(p) a=s.read_flat(0,s.volume) print({'schematic_bytes':p.stat().st_size,'non_air_blocks':int((a != 0).sum()),'size_yzx':s.size_yzx}) for name in ('generate.py','render.mjs','test/test_manor.py','previews/front.png','previews/rear.png','previews/side.png','previews/top.png','previews/approach.png','previews/hall.png','previews/bedrooms.png','previews/undercroft.png','previews/study.png'): p=Path(name) print(name,p.stat().st_size) PY
mediterranean-villa — 25 条命令,3 条失败
[00] pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g '!node_modules' /work/generator/workspace /opt/tools /work | head -60 [01] cat /work/AGENTS.md /work/generator/workspace/AGENTS.md /opt/tools/template_generator.py /opt/tools/template_render.mjs [02] cp /opt/tools/template_generator.py generate.py cp /opt/tools/template_render.mjs render.mjs rg -n 'entity|block_entity|to_litematica|entities' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio -g '*.py' | head -85 sed -n '1,100p' /opt/tools/cutaway.py head -35 /work/generator/MCRender/resources/1.21.1-blocks.json [03] sed -n '1,220p' /work/generator/MCIO/mcio/sketch.py sed -n '1,260p' /work/generator/MCIO/mcio/litematica.py rg -n 'class (Compound|List|IntArray)|class Entity' /work/generator/MCIO/mcio/nbt/tag.py /work/generator/MCIO/mcio -g '*.py' [04] rg -n 'def save|def to_nbt|entities|Entities|TileEntities' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt/nbt.py sed -n '600,770p' /work/generator/MCIO/mcio/litematica.py sed -n '800,848p' /work/generator/MCIO/mcio/nbt/tag.py sed -n '50,180p' /work/generator/MCIO/mcio/nbt/nbt.py [05] sed -n '1310,1345p' /work/generator/MCIO/mcio/litematica.py sed -n '250,340p' /work/generator/MCIO/mcio/nbt/nbt.py [06] cat > /tmp/villa_methods.txt <<'EOF' def p(self, x: int, y: int, z: int, name: str, **properties: str) -> None: """Place a vanilla block using its short name. Args: x, y, z: Block coordinates. name: Vanilla block name. **properties: Blockstate properties. """ self.put(x, y, z, f"minecraft:{name}", **properties) def fill(self, bounds: tuple, name: str, **properties: str) -> None: """Fill a named architectural volume. Args: bounds: Inclusive XYZ bounds. name: Vanilla block name. **properties: Blockstate properties. """ self.box(bounds, f"minecraft:{name}", **properties) def mix(self, bounds: tuple, names: list) -> None: """Texture a surface with the seeded palette. Args: bounds: Inclusive XYZ bounds. names: Weighted vanilla material names. """ self.texture(bounds, [f"minecraft:{name}" for name in names]) def door(self, x: int, y: int, z: int, facing: str, hinge: str = "left", wood: str = "warped") -> None: """Install a complete closed door. Args: ...(截断) [07] chmod u+w generate.py render.mjs python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text() s=s.replace('from mcio.schematic import load_schematic', 'from mcio.nbt.nbt import File as NBTFile\nfrom mcio.nbt.tag import Compound, Int, Byte, Float, Double, String, IntArray, List as NBTList\nfrom mcio.schematic import load_schematic') s=s.replace('(45, 40, 35)', '(35, 27, 31)') s=s.replace('"name": "build"', '"name": "Villa_al_Mare"') s=s.replace('"description": "Generated build"', '"description": "Furnished Mediterranean villa, two floors, two pergolas and a garden pool. Front faces south (+Z)."') s=s.replace('Position = Tuple[int, int, int]', '''PLASTER = ["white_concrete"] * 7 + ["smooth_quartz"] * 3 + ["calcite"] FLOOR = ["oak_planks"] * 6 + ["birch_planks"] + ["stripped_oak_log"] DIRECTIONS = {"north": (0, -1), "south": (0, 1), "west": (-1, 0), "east": (1, 0)} Position = Tuple[int, int, int]''') s=s.replace('self.placed: Dict[Position, str] = {}', 'self.placed: Dict[Position, str] = {}\n self.entities = []\n self.block_entities = []') start=s.index(' def build(self)') end=s.index(' def export(',start) s=s[:start]+Path('/tmp/villa_methods.txt ...(截断) [08] # exit 1 python - <<'PY' from generate import Build, load_schematic from pathlib import Path b=Build().build() s=load_schematic(Path('output.litematic')) a=s.read_flat(0,s.volume).reshape(s.size_yzx) for x,y,z in ((33,2,5),(21,5,16),(7,12,7)): print((x,y,z),b.get(x,y,z),s.palette[int(a[y,z,x])]) PY pgrep -af vite-node [09] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [10] python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('return self.canvas.material(name, **properties)', 'return self.canvas.material(name, **dict(sorted(properties.items())))') s=s.replace('self.rng.choice(["potted_azalea_bush", "potted_flowering_azalea_bush", "potted_cypress"] if False else ["potted_azalea_bush", "potted_flowering_azalea_bush"])','self.rng.choice(["potted_azalea_bush", "potted_flowering_azalea_bush"])') s=s.replace('"potted_amethyst" if False else "potted_azalea_bush"','"potted_azalea_bush"') p.write_text(s) PY /opt/venv/bin/python generate.py [11] python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text() s=s.replace('gable = x in (x0 + 1, x1 - 1)', 'gable = x in (x0, x0 + 1, x1 - 1, x1)') s=s.replace('["brick", "brick", "brick", "red_sandstone"]','["brick"] * 9 + ["red_sandstone"]') s=s.replace('self.frame(18, 7, 6, "south", "clock")', 'self.frame(18, 7, 5, "south", "clock")') s=s.replace('self.frame(15, 13, 6, "south", "blaze_powder")', 'self.frame(15, 13, 5, "south", "blaze_powder")') s=s.replace('self.p(x, 8, 22, "vine", north="true")','self.p(x, 8, 22, "vine", up="true")') s=s.replace('if distance == 3:\n', 'if distance == 2:\n self.leaf(x + dx * distance, crown, z + dz * distance)\n if distance == 3:\n') s=s.replace(' self.leaf(x + dx, crown, z + dz)', ' self.leaf(x + dx, crown, z)\n self.leaf(x + 2 * dx, crown, z + dz)\n self.leaf(x + dx, crown, z + dz)') s=s.replace(' self.window(10, 6, 16, "south")', ''' # Cool blue plaster panels recall the reference's blue-and-white elevations. for bounds in ((7, 11, 5, 7, 15, 15), (24, 11, 5, 24, 15, 15), (8, 11, 16, 12, 15, 16), (31, 5, 9, 31, 9, 14), (25, 5, 1 ...(截断) [12] # exit 1 python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace(' self.window(24, 12, 12, "east")\n','') s=s.replace('self.roof(24, 32, 7, 16, 11)', 'self.roof(25, 32, 7, 16, 11)') p.write_text(s) PY mkdir -p test cat > test/test_villa.py <<'EOF' # Validate the delivered Minecraft volume, navigation, attached decorations and serialized entities. from collections import deque from pathlib import Path import sys import tempfile import json import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG, DIRECTIONS from mcio.schematic import load_schematic from mcio.nbt.nbt import File as NBTFile RULES = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) ROOMS = { 'living': ((8, 5, 5, 16, 9, 15), (13, 5, 12)), 'first_hall': ((18, 5, 5, 23, 9, 15), (22, 5, 12)), 'enchanting': ((25, 5, 9, 30, 9, 14), (27, 5, 11)), 'kitchen': ((8, 11, 5, 13, 15, 15), (12, 11, 11)), 'brewing': ((15, 11, 5, 18, 15, 9), (17, 11, 8)), 'bedroom': ((20, 11, 5, 23, 15, 9), (23, 11, 9)), 'second_hall': ((14, 11, 11, 23, 15, 15), (21, 11, 13)), } def decode(st ...(截断) [13] python - <<'PY' from test.test_villa import World, reachable_nodes, walk_nodes w=World(); r=reachable_nodes(w); n=walk_nodes(w) for z in range(8,16): y=z-3 if z>8 else 5 for x in (15,16): p=(x,y,z) print(p,'node',p in n,'reach',p in r,'feet/head/up',w.at(*p)[0],w.at(x,y+1,z)[0],w.at(x,y+2,z)[0]) print('reached upper', sorted(p for p in r if p[1]==11)[:20]) PY [14] /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/first_floor.litematic --max-y 7 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/second_floor_low.litematic --max-y 13 /opt/venv/bin/python /opt/tools/cutaway.py --input previews/second_floor_low.litematic --output previews/second_floor.litematic --axis y --max 9 --keep-above xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/first_floor.litematic --output previews/first_floor.png --size 900 --azimuth 0 --elevation 76 --radius 3.0 --background 157,208,224,255 [15] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [16] # exit 1 python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('self.fill((15, 10, 12, 16, 10, 14), "air")','self.fill((15, 10, 11, 16, 10, 14), "air")') s=s.replace('self.door(17, 11, 10, "south", wood="birch")','self.door(18, 11, 10, "south", wood="birch")') s=s.replace('for z in range(12, 15):\n self.p(x, 11, z, "oak_fence"','for z in range(11, 15):\n self.p(x, 11, z, "oak_fence"') s=s.replace('self.lamp(x1 - 2, beam - 2, z1)','self.lamp(x1 - 2, beam - 2, z1 - ((z1 - z0) % 2))') p.write_text(s) PY /opt/venv/bin/python generate.py /opt/venv/bin/python -m pytest -q test/test_villa.py [17] python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('self.fill((x0, beam, z, x1, beam, z), "oak_slab", type="top")','self.fill((x0 + 1, beam, z, x1 - 1, beam, z), "oak_slab", type="bottom")') s=s.replace(' self.lamp(28, 8, 11)',' self.frame(30, 8, 14, "north", "amethyst_shard")\n self.lamp(28, 8, 11)') s=s.replace(' self.lamp(21, 14, 13)',' self.frame(18, 13, 15, "north", "compass")\n self.lamp(21, 14, 13)') s=s.replace('self.p(12, 11, 19, "oak_slab", type="top")','self.p(12, 11, 19, "oak_planks")') s=s.replace('self.p(28, 5, 18, "oak_slab", type="top")','self.p(28, 5, 18, "oak_planks")') s=s.replace(''' for x, z in ((27, 10), (28, 10), (29, 10), (27, 11), (29, 11), (27, 12), (28, 12), (29, 12)): self.p(x, 5, z, "purple_carpet") # Enchanting gaps must remain air for bookshelf power; rug lives at the entry instead. self.fill((27, 5, 10, 29, 5, 10), "air") for x, z in ((27, 11), (29, 11), (27, 12), (28, 12), (29, 12)): self.p(x, 5, z, "air") ''',''' # Enchanting gaps remain air for bookshelf power; the rug lives at the entry. ''') s=s.replace(''' ...(截断) [18] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/second_floor.litematic --output previews/second_floor.png --size 900 --azimuth 0 --elevation 76 --radius 2.8 --background 157,208,224,255 [19] git status --short wc -l generate.py test/test_villa.py render.mjs sed -n '1,55p' generate.py [20] python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text() start=s.index('from __future__') s='# Generate and verify a furnished Java 1.21.1 Mediterranean villa from a seeded block plan.\n'+s[start:] s=s.replace('FLOOR = ["oak_planks"] * 6 + ["birch_planks"] + ["stripped_oak_log"]', 'FLOOR = ["oak_planks"] * 10 + ["birch_planks"] + ["bamboo_planks"] * 2') s=s.replace(''' return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), }''',''' root = NBTFile.load_regardless_of_gzipped(path) assert len(root["Regions"]) == 1, "export must have exactly one region" region = root["Regions"][CONFIG["name"]] assert region["Entities"] == NBTList[Compound](self.entities), "item frames changed on reload" assert region["TileEntities"] == NBTList[Compound](self.block_entities), "furnished block entities changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": int(root["Metadata"]["TotalBlocks"]), "palette_states": len(loaded.palette), "item_frames": ...(截断) [21] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/second_floor.litematic --output previews/second_floor.png --size 900 --azimuth 0 --elevation 76 --radius 2.8 --background 157,208,224,255 [22] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/stair_section.litematic --output previews/stair_section.png --size 900 --azimuth 90 --elevation 12 --radius 3.0 --background 157,208,224,255 [23] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/first_floor.litematic --output previews/first_floor.png --size 900 --azimuth 0 --elevation 76 --radius 3.15 --background 157,208,224,255 [24] xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews

token 花在哪

归因方式与前三轮一致:看每个请求触发的命令,取最强的动作。没有命令的请求算纯推理。

Fig 9. 按用途聚合的新增输入 token。缓存命中的输入不计入此图。
Fig 10. 按用途聚合的输出 token(含推理)。

新增输入 token 按阶段

阶段root-housesandstone-cottagestone-cottagered-roof-farmsteadstilt-water-cabinstilted-manormerchant-manormediterranean-villa合计
探索环境与 API33,86816,79910,16310,77215,6416,04130,08722,437145,808
写生成器12,56117,47027,31413,9036,0768,9428,8829,136104,284
渲染看图24,1396,9243,75215,1206,40118,27724,74427,705127,062
写测试与跑测试16,29054,5334,0417,29011,65115,11525,91410,631145,465
修补与格式化0028106,4970006,778
验证导出6,154005,6522,09412,7024,797031,399
规划与说明11,5125,48510,69714,27818,42712,43010,46111,30694,596

输出 token 按阶段

阶段root-housesandstone-cottagestone-cottagered-roof-farmsteadstilt-water-cabinstilted-manormerchant-manormediterranean-villa合计
探索环境与 API1,9802,1192,8464,4932,9971581,4933,03319,119
写生成器12,5129,8703,0919,1657,10915,75216,55112,74386,793
渲染看图2,1001,9102,2303,8944,2124,9893,8304,63327,798
写测试与跑测试15,1088,5458,0188,4108,76113,08515,92213,51491,363
修补与格式化004,21701160004,333
验证导出4,505001,0141,1731,17596708,834
规划与说明3,6629095,4221,9501,3422,6115142,89919,309

两张表的分布形状不同:新增输入集中在『探索』和『渲染看图』(读文件、读渲染结果), 输出集中在『写生成器』和『写测试』(真正在产代码)。这也是为什么按输入算和按输出算会得出不同的『瓶颈』。

逐请求细切(按时间)

root-house 逐请求细切 — 30 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。
sandstone-cottage 逐请求细切 — 19 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。
stone-cottage 逐请求细切 — 24 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。
red-roof-farmstead 逐请求细切 — 20 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。
stilt-water-cabin 逐请求细切 — 22 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。
stilted-manor 逐请求细切 — 25 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。
merchant-manor 逐请求细切 — 27 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。
mediterranean-villa 逐请求细切 — 20 个请求
每段是一个模型请求,左起为时间顺序。hover 看阶段、命令、token 与估算金额。

花费估算

费率:新增输入 $1.25/M,缓存输入 $0.125/M,输出 $10/M。 这是估算,实际走订阅额度,真实支出为 0。

build缓存输入缓存费新增输入新增输入费 输出输出费合计
root-house 1,830,528$0.229 104,524$0.131 39,867$0.399 $0.758
sandstone-cottage 780,416$0.098 101,211$0.127 23,353$0.234 $0.458
stone-cottage 1,052,288$0.132 56,248$0.070 25,824$0.258 $0.460
red-roof-farmstead 990,080$0.124 67,015$0.084 28,926$0.289 $0.497
stilt-water-cabin 1,085,824$0.136 66,787$0.083 25,710$0.257 $0.476
stilted-manor 1,300,224$0.163 73,507$0.092 37,770$0.378 $0.632
merchant-manor 1,738,880$0.217 104,885$0.131 39,277$0.393 $0.741
mediterranean-villa 1,185,152$0.148 81,215$0.102 36,822$0.368 $0.618
合计9,963,392$1.245 655,392$0.819 257,549$2.575 $4.640

问题与分类

分类沿用前三轮的 A–E,这一轮多出一个 F 导出重载不一致:模板 verify() 在重新加载时 发现某个坐标的方块和内存里记的不一样,原因都是同一个坐标被写了两次。

类别root-housesandstone-cottagestone-cottagered-roof-farmsteadstilt-water-cabinstilted-manormerchant-manormediterranean-villa合计
A 写文件时的语法或导入错11
C 自写测试失败3112135218
E 其它11
F 导出重载不一致1113
Fig 11. 失败命令按类别堆叠,每个 build 一根柱(次)。

结论很集中:模型的主要失败来源是它自己写的测试,占 18/23。这不是坏事——测试先于修复被写出来, 失败一次就改一次生成器。真正暴露工具缺陷的是 F 那三次:模板允许重复写同一格而不报错, 直到导出后重载才炸。这一条应该由 put() 在写入时就挡掉。

逐条明细

root-house — 3 条

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text();s=s.replace("self.put(19, 9, 15, 'minecraft:dark_oak_planks')\n self.put(18, 10, 15, 'minecraft:dark_oak_slab', type='to --- decorations_supported_and_foliage_connected ERROR test/test_structure.py::test_window_and_door_openings_remain_clear - At... ERROR test/test_structure.py::test_all_living_zones_have_nearby_light_and_furnishings ERROR test/test_structure.py::test_inventory_nbt_survives_export - AttributeE... 1 passed, 8 errors in 1.07s

C 自写测试失败 — exit 1

sed -i 's/NBTFile.from_buffer(stream).root/NBTFile.parse(stream)/' test/test_structure.py /opt/venv/bin/python -m pytest -q test --- (11, 3, 8) in {(3, 2, 1), (4, 2, 0), (4, 2, 1), (4, 2, 2), (5, 2, 0), (5, 2, 1), ...} test/test_structure.py:221: AssertionError =========================== short test summary info ============================ FAILED test/test_structure.py::test_every_room_and_floor_reachable_with_headroom 1 failed, 8 passed in 1.25s

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace("self.put(6, 3, 16, 'minecraft:furnace'", "self.put(6, 3, 14, 'minecraft:furnace'") s=s.replace("self.put(15, 5, 10 --- 70>) test/test_structure.py:358: AssertionError =========================== short test summary info ============================ FAILED test/test_structure.py::test_every_room_and_floor_reachable_with_headroom FAILED test/test_structure.py::test_household_utilities_accessible - Assertio... 2 failed, 9 passed in 1.37s
sandstone-cottage — 2 条

F 导出重载不一致 — exit 1

chmod u+w generate.py render.mjs cat > /tmp/adapt_generator.py <<'PY' from pathlib import Path p = Path('generate.py') s = p.read_text() a=s.index(' def build(') b=s.index(' def export(', a) s=s[:a]+Path('/tmp/buil --- (path) ^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 534, in verify assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: cell (x=15, y=3, z=0) changed on reload

C 自写测试失败 — exit 1

mkdir -p test cat > test/test_structure.py <<'PY' # Verify the saved cottage's geometry, circulation, decoration support and deterministic export. from collections import deque from pathlib import Path import json import --- o ============================ FAILED test/test_structure.py::test_every_room_and_outside_approach_reachable[front_path-point10] FAILED test/test_structure.py::test_decorations_are_supported - AssertionErro... FAILED test/test_structure.py::test_required_materials_and_furniture - Assert... 3 failed, 22 passed in 1.32s
stone-cottage — 2 条

F 导出重载不一致 — exit 1

/opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py --- y(path) ^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 350, in verify assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: cell (x=3, y=3, z=6) changed on reload

C 自写测试失败 — exit 1

/opt/venv/bin/python -m yapf -i generate.py test/test_cottage.py && /opt/venv/bin/python generate.py && /opt/venv/bin/python -m pytest -q test --- E + where full = <test_cottage.Volume object at 0x75c3b6bef980>.full test/test_cottage.py:189: AssertionError =========================== short test summary info ============================ FAILED test/test_cottage.py::test_closed_envelope_and_solid_roof_backing - As... 1 failed, 10 passed in 0.56s
red-roof-farmstead — 3 条

C 自写测试失败 — exit 1

mkdir -p test cat > test/test_farmstead.py <<'PY' # Validate the delivered farmstead's geometry, supported details, and reproducibility. from __future__ import annotations from collections import deque import gzip impor --- st_windows_and_doorway_have_clear_openings FAILED test/test_farmstead.py::test_decorations_have_support_and_structure_is_anchored FAILED test/test_farmstead.py::test_each_room_has_nearby_floor_lighting - Ass... FAILED test/test_farmstead.py::test_farm_features_and_four_sails - AssertionE... 7 failed, 3 passed in 1.01s

C 自写测试失败 — exit 1

sed -n '220,285p' /work/generator/MCIO/mcio/nbt/nbt.py /opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace("for z in range(18, 25):\n self.fill((x, h - --- 40>) test/test_farmstead.py:327: AssertionError =========================== short test summary info ============================ FAILED test/test_farmstead.py::test_single_region_version_and_scale - gzip.Ba... FAILED test/test_farmstead.py::test_each_room_has_nearby_floor_lighting - Ass... 2 failed, 8 passed in 1.03s

A 写文件时的语法或导入错 — exit 1

rg -n 'def save' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt/nbt.py sed -n '960,1015p' /work/generator/MCIO/mcio/litematica.py sed -n '282,320p' /work/generator/MCIO/mcio/nbt/nbt.py /opt/venv/bi --- open_file = gzip.open if gzipped else open with open_file(filename, "wb") as fileobj: self.write(fileobj, byteorder or self.byteorder) def __enter__(self): return self Traceback (most recent call last): File "<stdin>", line 1, in <module> ModuleNotFoundError: No module named 'mcio'
stilt-water-cabin — 2 条

F 导出重载不一致 — exit 1

chmod u+w generate.py render.mjs && python /tmp/implement_cabin.py && /opt/venv/bin/python -m yapf -i generate.py && /opt/venv/bin/python generate.py --- (path) ^^^^^^^^^^^^^^^^^^ File "/work/generator/workspace/generate.py", line 432, in verify assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: cell (x=12, y=8, z=6) changed on reload

C 自写测试失败 — exit 1

mkdir -p test cat > test/test_cabin.py <<'PY' # Verify the exported cabin geometry, access, attachments, entity data, and determinism. from __future__ import annotations from collections import deque import json from pa --- ? ^^^^^^ E + minecraft:oak_log E ? ^^^ test/test_cabin.py:183: AssertionError =========================== short test summary info ============================ FAILED test/test_cabin.py::test_timber_platform_and_piles_leave_water_visible 1 failed, 9 passed in 0.59s
stilted-manor — 3 条

C 自写测试失败 — exit 1

cat > test/test_structure.py <<'PY' # Validate the exported manor's geometry, circulation, state pairs, and reproducibility. from collections import deque import hashlib import json from pathlib import Path import sys im --- st_envelope_sealed_including_nonfull_roof_blocks FAILED test/test_structure.py::test_windows_and_doorways_clear - AssertionErr... FAILED test/test_structure.py::test_real_overhang_and_braced_support - assert... FAILED test/test_structure.py::test_rooms_have_nearby_lighting - AssertionErr... 5 failed, 4 passed in 0.61s

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text() s=s.replace("for z in range(6, 12):\n self.put(x, top - 1, z", "for z in range(6, 12):\n if top < --- not [(6, 10, 7), (6, 10, 8), (6, 10, 9), (6, 10, 10), (6, 10, 11), (6, 10, 12), ...] test/test_structure.py:213: AssertionError =========================== short test summary info ============================ FAILED test/test_structure.py::test_envelope_sealed_including_nonfull_roof_blocks 1 failed, 8 passed in 0.75s

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace("[(9, 11), (16, 18), (21, 22)]", "[(8, 10), (16, 18), (21, 22)]") s=s.replace("['stone_brick'] * 5 + ['cobblestone'] * --- + where full = <test_structure.Volume object at 0x7ee674f99eb0>.full test/test_structure.py:253: AssertionError =========================== short test summary info ============================ FAILED test/test_structure.py::test_windows_and_doorways_clear - AssertionErr... 1 failed, 8 passed in 0.66s
merchant-manor — 5 条

C 自写测试失败 — exit 1

cat > test/test_manor.py <<'PY' # Validate the exported manor's structure, circulation, envelope, furnishings and deterministic NBT. from __future__ import annotations from collections import deque from pathlib import P --- test_windows_and_entry_clearance - AssertionError:... FAILED test/test_manor.py::test_lighting_near_each_room_floor - AssertionErro... FAILED test/test_manor.py::test_stocked_containers_frames_and_lectern_nbt - A... FAILED test/test_manor.py::test_rerun_is_byte_reproducible_and_reload_exact 6 failed, 4 passed in 0.85s

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace('19, max(10, y + 3), z','19, y + 3, z').replace('10, max(17, y + 3), z','10, y + 3, z').replace("self.fill((8, 15, 18, --- t test summary info ============================ FAILED test/test_manor.py::test_all_rooms_and_front_stairs_are_reachable - As... FAILED test/test_manor.py::test_windows_and_entry_clearance - AssertionError:... FAILED test/test_manor.py::test_lighting_near_each_room_floor - AssertionErro... 3 failed, 7 passed in 0.69s

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace("self.window(7, 18, 18, 2, 3, 'west')", "self.window(7, 18, 17, 1, 3, 'west')").replace(" self.window(15, 17, 26 --- 17), 'east bedroom': (25, 16, 19), 'map room': (19, 16, 22), 'study': (20, 23, 22), ...} test/test_manor.py:243: AssertionError =========================== short test summary info ============================ FAILED test/test_manor.py::test_all_rooms_and_front_stairs_are_reachable - As... 1 failed, 9 passed in 0.63s

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace(" self.fill((9, 15, 20, 10, 15, 20), 'spruce_planks')\n", '').replace("self.door(8, 16, 19, facing='north')", "s --- ject test_all_rooms_and_front_stairs_are_reachable.<locals>.<genexpr> at 0x76a019044110>) test/test_manor.py:251: AssertionError =========================== short test summary info ============================ FAILED test/test_manor.py::test_all_rooms_and_front_stairs_are_reachable - As... 1 failed, 9 passed in 0.64s

C 自写测试失败 — exit 1

/opt/venv/bin/python - <<'PY' from pathlib import Path p=Path('generate.py');s=p.read_text().replace(" self.p(x, 9, 24, 'spruce_stairs', facing='south')", " if x == 12:\n self.p(x, 9, --- ject test_all_rooms_and_front_stairs_are_reachable.<locals>.<genexpr> at 0x778d5d320110>) test/test_manor.py:251: AssertionError =========================== short test summary info ============================ FAILED test/test_manor.py::test_all_rooms_and_front_stairs_are_reachable - As... 1 failed, 9 passed in 0.59s
mediterranean-villa — 3 条

E 其它 — exit 1

python - <<'PY' from generate import Build, load_schematic from pathlib import Path b=Build().build() s=load_schematic(Path('output.litematic')) a=s.read_flat(0,s.volume).reshape(s.size_yzx) for x,y,z in ((33,2,5),(21,5, --- tance=1] minecraft:jungle_leaves[distance=1,persistent=true] (21, 5, 16) minecraft:warped_door[half=lower,facing=south,hinge=left,open=false,powered=false] minecraft:warped_door[facing=south,half=lower,hinge=left,open=false,powered=false] (7, 12, 7) minecraft:light_blue_stained_glass minecraft:light_blue_stained_glass

C 自写测试失败 — exit 1

python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace(' self.window(24, 12, 12, "east")\n','') s=s.replace('self.roof(24, 32, 7, 16, 11)', 'self.roof(25, 32, 7, 16, 11)') p.write_t --- ' != 'air' test/test_villa.py:290: AssertionError =========================== short test summary info ============================ FAILED test/test_villa.py::test_all_rooms_and_terraces_reachable_with_headroom FAILED test/test_villa.py::test_all_decorations_have_support - AssertionError... 2 failed, 9 passed in 0.51s

C 自写测试失败 — exit 1

python - <<'PY' from pathlib import Path p=Path('generate.py') s=p.read_text().replace('self.fill((15, 10, 12, 16, 10, 14), "air")','self.fill((15, 10, 11, 16, 10, 14), "air")') s=s.replace('self.door(17, 11, 10, "south" --- ion': Byte(0), 'ItemDropChance': Float(1.0), 'Fixed': Byte(0), 'Invisible': Byte(0)})])) test/test_villa.py:308: AssertionError =========================== short test summary info ============================ FAILED test/test_villa.py::test_all_decorations_have_support - AssertionError... 1 failed, 10 passed in 0.50s

测试对比

八个 build 的测试函数按它检查什么归类。分类是按函数名里的关键词做的,一个函数只归一类。

测试目的root-housesandstone-cottagestone-cottagered-roof-farmsteadstilt-water-cabinstilted-manormerchant-manormediterranean-villa合计
可达性与流线4423344226
封闭性与外壳111111129
支撑与悬空112121311
光照11111117
家具与内饰2215
尺寸与比例11111117
材质与状态111115
确定性与导出2211111110
其它112116
Fig 12. 测试目的分布,每个 build 一根堆叠柱(个)。
Fig 13. 测试行数与建筑体积的关系:体积跨 15 倍,测试行数几乎不动。

八个 build 无一例外都覆盖了『可达性与流线』『封闭性与外壳』『支撑与悬空』『确定性与导出』这四类, 这正是 AGENTS.md 和 PROMPT.md 明确要求的四类。超出要求自发写的主要是『光照』与『家具与内饰』, 而这两类恰好是渲染图看不出来的——模型知道哪些东西必须靠测试兜。

每个 build 的测试

root-house — 11 个测试 / 360 行 / test_structure.py

test_version_region_size_and_vanilla_states 尺寸与比例
test_complete_doors_beds_and_double_plants 可达性与流线
test_closed_envelope_separates_all_room_air_from_sky 封闭性与外壳
test_every_room_and_floor_reachable_with_headroom 可达性与流线
test_decorations_supported_and_foliage_connected 支撑与悬空
test_window_and_door_openings_remain_clear 可达性与流线
test_all_living_zones_have_nearby_light_and_furnishings 光照
test_inventory_nbt_survives_export 确定性与导出
test_generator_reruns_without_arguments_and_is_byte_reproducible 确定性与导出
test_bucket_frame_is_attached_and_contains_bucket 其它
test_household_utilities_accessible 可达性与流线

# Validate the exported Root House, including movement, support, enclosure, and deterministic bytes. from __future__ import annotations import gzip import hashlib import json import shutil import subprocess import sys import tempfile from collections import deque from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, '/work/generator/MCIO') from generate import MAIN_ROWS as PLAN_MAIN_ROWS, LOFT_ROWS as PLAN_LOFT_ROWS MAIN_ROWS = {z: (24 - right, 24 - left) for z, (left, right) in PLAN_MAIN_ROWS.items()} LOFT_ROWS = {z: (24 - right, 24 - left) for z, (left, right) in PLAN_LOFT_ROWS.items()} from mcio.schematic import load_schematic from mcio.nbt.nbt import File as NBTFile DIRECTIONS = {'north': (0, 0, -1), 'south': (0, 0, 1), 'west': (-1, 0, 0), 'east': (1, 0, 0)} NEIGHBORS = tuple(DIRECTIONS.values()) + ((0, 1, 0), (0, -1, 0)) AIR = {'air', 'cave_air', 'void_air'} PLANTS = {'fern', 'short_grass', 'large_fern', 'sweet_berry_bush', 'azalea'} SOIL = {'grass_block', 'dirt', 'rooted_dirt', 'coarse_dirt', 'moss_block', 'podzol'} def plan(p): """Map the design coordinates to the reference-facing exported orientation.""" x, y, z = p return 24 - x, y, z def add(a, b): """Add two XYZ coordinates.""" return tuple(x + y for x, y in zip(a, b)) def parse(state): """Separate a blockstate into a short name and properties.""" name, _, props = state.partition('[') return name.removeprefix('minecraft:'), dict(p.split('=') for p in props.rstrip(']').split(',') if p) class World: """Read all checks from the persisted schematic.""" def __init__(self, path): self.source = load_schematic(path) self.sy, self.sz, self.sx = self.source.size_yzx self.ids = self.source.read_flat(0, self.source.volume).reshape(self.source.size_yzx) self.palette = [parse(s) for s in self.source.palette] with gzip.open(path, 'rb') as stream: self.nbt = NBTFile.parse(stream) def inside(self, p): x, y, z = p return 0 <= x < self.sx and 0 <= y < self.sy and 0 <= z < self.sz def get(self, p): if not self.inside(p): return 'air', {} x, y, z = p return self.palette[int(self.ids[y, z, x])] def cells(self): for y in range(self.sy): for z in range(self.sz): for x in range(self.sx): p = (x, y, z) yield p, *self.get(p) def full(self, p): name, props = self.get(p) if name in AIR | PLANTS | {'vine', 'lantern', 'campfire', 'candle', 'flower_pot', 'water_cauldron', 'composter', 'chest'}: return False if name.startswith('potted_') or name.endswith(('_fence', '_stairs', '_carpet', '_bed', '_trapdoor', '_pressure_plate', '_leaves', '_wall')): return False if name.endswith('_slab'): return props.get('type') == 'double' if name.endswith('_door'): return props.get('open') == 'false' return True def top_support(self, p): name, props = self.get(p) return self.full(p) or (name.endswith('_slab') and props.get('type') in {'top', 'double'}) or (name.endswith('_stairs') and props.get('half') == 'top') def passable(self, p): name, _ = self.get(p) return name in AIR or name.endswith(('_door', '_carpet')) def standable(self, p): if not (self.passable(p) and (self.get(add(p, (0, 1, 0)))[0] in AIR or self.get(add(p, (0, 1, 0)))[0].endswith('_door'))): return False below = add(p, (0, -1, 0)) name, props = self.get(below) return self.top_support(below) or (name.endswith('_stairs') and props.get('half') == 'bottom') @pytest.fixture(scope='module') def world(): return World(ROOT / 'output.litematic') def test_version_region_size_and_vanilla_states(world): assert int(world.nbt['MinecraftDataVersion']) == 3955 assert len(world.nbt['Regions']) == 1 assert (world.sx, world.sy, world.sz) == (25, 23, 22) assert int(world.nbt['Metadata']['TimeCreated']) == 0 authority = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for name, props in world.palette: assert name in authority, name for key, value in props.items(): assert key in authority[name][0], (name, key) assert value in authority[name][0][key], (name, key, value) region = next(iter(world.nbt['Regions'].values())) assert all(int(region['Position'][a]) == 0 for a in 'xyz') def test_complete_doors_beds_and_double_plants(world): doors, beds = 0, 0 for p, name, props in world.cells(): if name.endswith('_door'): other = add(p, (0, 1 if props['half'] == 'lower' else -1, 0)) other_name, other_props = world.get(other) assert other_name == name assert other_props['half'] != props['half'] assert all(other_props[k] == props[k] for k in ('facing', 'hinge', 'open', 'powered')) if props['half'] == 'lower': assert world.top_support(add(p, (0, -1, 0))) doors += 1 if name.endswith('_bed'): direction = DIRECTIONS[props['facing']] other = add(p, direction if props['part'] == 'foot' else tuple(-n for n in direction)) other_name, other_props = world.get(other) assert other_name == name assert other_props['part'] != props['part'] assert other_props['facing'] == props['facing'] assert world.top_support(add(p, (0, -1, 0))) beds += props['part'] == 'head' if name == 'large_fern': other = add(p, (0, 1 if props['half'] == 'lower' else -1, 0)) assert world.get(other)[0] == name assert world.get(other)[1]['half'] != props['half'] assert doors == 2 assert beds == 2 def interior_cells(): """Enumerate the designed rooms, excluding the outdoor cave vestibule.""" for rows, heights in ((MAIN_ROWS, range(3, 7)), (LOFT_ROWS, range(8, 12))): for z, (left, right) in rows.items(): for x in range(left, right + 1): for y in heights: yield x, y, z for y in (12, 13, 14): for z in range(10, 16): for x in range(11, 14): yield x, y, z def test_closed_envelope_separates_all_room_air_from_sky(world): # Flood from every boundary; partial blocks are conservatively treated as leaks. exterior = set() todo = deque() for p, _, _ in world.cells(): x, y, z = p if x in (0, world.sx - 1) or y in (0, world.sy - 1) or z in (0, world.sz - 1): if not world.full(p): exterior.add(p) todo.append(p) while todo: p = todo.popleft() for delta in NEIGHBORS: q = add(p, delta) if q not in exterior and world.inside(q) and not world.full(q): exterior.add(q) todo.append(q) leaks = [p for p in interior_cells() if world.get(p)[0] in AIR and p in exterior] assert not leaks, f'Interior air connected to exterior: {leaks[:20]}' def walk_reachable(world): """Traverse floor cells with two-block clearance and direction-correct stair rises.""" start = (11, 2, 0) assert world.standable(start) reached = {start} todo = deque([start]) while todo: p = todo.popleft() for facing, (dx, _, dz) in DIRECTIONS.items(): for dy in (-1, 0, 1): q = add(p, (dx, dy, dz)) if q in reached or not world.inside(q) or not world.standable(q): continue if dy: high = q if dy > 0 else p name, props = world.get(add(high, (0, -1, 0))) uphill = facing if dy > 0 else {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'}[facing] if not name.endswith('_stairs') or props.get('facing') != uphill: continue if world.get((p[0], max(p[1], q[1]) + 1, p[2]))[0] not in AIR: continue if world.get((q[0], max(p[1], q[1]) + 1, q[2]))[0] not in AIR: continue reached.add(q) todo.append(q) return reached def test_every_room_and_floor_reachable_with_headroom(world): reached = walk_reachable(world) destinations = { 'door': (11, 3, 8), 'hearth': (9, 3, 16), 'workbench': (7, 3, 11), 'pantry': (14, 3, 17), 'sleeping': (11, 8, 14), 'writing_nook': (13, 8, 11), 'loft_storage': (14, 8, 16), 'landing': (17, 8, 16), } for purpose, p in destinations.items(): assert plan(p) in reached, (purpose, plan(p)) unreachable = [] for rows, y in ((MAIN_ROWS, 3), (LOFT_ROWS, 8)): for z, (left, right) in rows.items(): for x in range(left, right + 1): p = x, y, z if world.standable(p) and p not in reached: unreachable.append(p) assert not unreachable, f'Unreachable floor: {unreachable}' for z in range(11, 16): for x in (17, 18): assert plan((x, z - 7, z)) in reached, ('stairs', plan((x, z - 7, z))) def test_decorations_supported_and_foliage_connected(world): failures = [] for p, name, props in world.cells(): below = add(p, (0, -1, 0)) above = add(p, (0, 1, 0)) valid = True if name in PLANTS: if props.get('half') == 'upper': valid = world.get(below)[0] == name else: valid = world.get(below)[0] in SOIL elif name == 'lantern': valid = world.full(above) if props['hanging'] == 'true' else world.top_support(below) elif name == 'vine': for direction, delta in DIRECTIONS.items(): if props.get(direction) == 'true': attached = add(p, delta) valid &= world.full(attached) or world.get(attached)[0].endswith('_leaves') elif name in {'candle', 'flower_pot', 'campfire', 'water_cauldron', 'composter', 'barrel', 'chest', 'bookshelf', 'crafting_table', 'loom', 'furnace', 'smoker'} or name.startswith('potted_') or name.endswith(('_carpet', '_pressure_plate')): valid = world.top_support(below) elif name.endswith('_fence'): valid = world.top_support(below) or world.get(below)[0].endswith('_fence') elif name.endswith('_trapdoor'): valid = world.top_support(below) or any(world.full(add(p, delta)) for delta in NEIGHBORS) if not valid: failures.append((p, name, world.get(below))) assert not failures, failures leaves = {p for p, name, _ in world.cells() if name.endswith('_leaves')} attached = {p for p in leaves if any(world.get(add(p, d))[0].endswith(('_log', '_wood')) for d in NEIGHBORS)} for _ in range(6): attached |= {p for p in leaves if any(add(p, d) in attached for d in NEIGHBORS)} assert leaves <= attached, leaves - attached def test_window_and_door_openings_remain_clear(world): for x in (12, 13): assert world.get((x, 3, 8))[0] == 'spruce_door' for y in (3, 4): assert world.get((x, y, 7))[0] in AIR assert world.get((x, y, 9))[0] in AIR glass = [(12 + dx, 13 + dy, 9) for dx, dy in ((0, 0), (-1, 0), (1, 0), (0, -1), (0, 1))] for p in glass: assert world.get(p)[0] == 'light_gray_stained_glass' assert world.get(add(p, (0, 0, 1)))[0] in AIR assert world.get(add(p, (0, 0, -1)))[0] in AIR for p in ((19, 4, 13), (19, 5, 14)): assert world.get(p)[0] == 'light_gray_stained_glass' assert world.get(add(p, (-1, 0, 0)))[0] in AIR for x in range(20, 24): assert world.get((x, p[1], p[2]))[0] in AIR for x in range(11, 14): for y in (4, 5): assert world.get((x, y, 19))[0] == 'light_gray_stained_glass' for z in (20, 21): assert world.get((x, y, z))[0] in AIR def test_all_living_zones_have_nearby_light_and_furnishings(world): lights = [p for p, name, props in world.cells() if name == 'lantern' or (name in {'campfire', 'candle'} and props.get('lit') == 'true')] zones = [(7, 3, 11), (9, 3, 16), (14, 3, 17), (12, 3, 9), (11, 8, 14), (13, 8, 11), (17, 8, 16)] for p in zones: assert min(sum(abs(a - b) for a, b in zip(plan(p), q)) for q in lights) <= 6, p names = {name for _, name, _ in world.cells()} assert {'green_bed', 'crafting_table', 'loom', 'smoker', 'furnace', 'campfire', 'bookshelf', 'barrel', 'chest', 'composter', 'water_cauldron', 'sweet_berry_bush', 'fern', 'vine'} <= names # Chest lids must open without a solid block immediately above. for p, name, _ in world.cells(): if name == 'chest': assert not world.full(add(p, (0, 1, 0))), p def test_inventory_nbt_survives_export(world): region = next(iter(world.nbt['Regions'].values())) inventories = region['TileEntities'] assert len(inventories) >= 7 seen_items = set() for entity in inventories: p = tuple(int(entity[axis]) for axis in 'xyz') assert str(entity['id']) == 'minecraft:' + world.get(p)[0] for item in entity['Items']: assert int(item['count']) > 0 seen_items.add(str(item['id'])) assert {'minecraft:bucket', 'minecraft:bread', 'minecraft:iron_axe', 'minecraft:book'} <= seen_items def test_generator_reruns_without_arguments_and_is_byte_reproducible(): with tempfile.TemporaryDirectory(prefix='.rerun-', dir=ROOT / 'test') as folder: clean = Path(folder) shutil.copyfile(ROOT / 'generate.py', clean / 'generate.py') hashes = [] for _ in range(2): result = subprocess.run([sys.executable, str(clean / 'generate.py')], cwd=clean, text=True, capture_output=True) assert result.returncode == 0, result.stderr hashes.append(hashlib.sha256((clean / 'output.litematic').read_bytes()).hexdigest()) assert hashes[0] == hashes[1] == hashlib.sha256((ROOT / 'output.litematic').read_bytes()).hexdigest() def test_bucket_frame_is_attached_and_contains_bucket(world): region = next(iter(world.nbt['Regions'].values())) frames = [e for e in region['Entities'] if str(e['id']) == 'minecraft:item_frame'] assert len(frames) == 1 frame = frames[0] p = tuple(int(frame['Tile' + axis.upper()]) for axis in 'xyz') assert int(frame['Facing']) == 2 assert world.get(p)[0] in AIR assert world.get(add(p, (0, 0, 1)))[0] == 'barrel' assert str(frame['Item']['id']) == 'minecraft:bucket' assert int(frame['Item']['count']) == 1 assert int(frame['Fixed']) == 1 def test_household_utilities_accessible(world): reached = walk_reachable(world) for p, name, _ in world.cells(): if name in {'barrel', 'chest', 'crafting_table', 'loom', 'furnace', 'smoker', 'composter', 'water_cauldron'}: assert any(add(p, d) in reached for d in DIRECTIONS.values()), (p, name) if name in {'composter', 'water_cauldron'}: assert world.get(add(p, (0, 1, 0)))[0] in AIR, (p, name)
sandstone-cottage — 14 个测试 / 313 行 / test_structure.py

test_single_region_version_size_and_metadata 尺寸与比例
test_all_states_are_vanilla_1211 材质与状态
test_matching_complete_doors 可达性与流线
test_complete_beds 家具与内饰
test_every_room_and_outside_approach_reachable 可达性与流线
test_every_usable_floor_cell_reachable 可达性与流线
test_stairs_have_correct_rise_and_clearance 可达性与流线
test_envelope_separates_all_inside_air_from_sky 封闭性与外壳
test_windows_are_clear_and_have_views 其它
test_decorations_are_supported 支撑与悬空
test_room_lights_near_the_floor 光照
test_required_materials_and_furniture 家具与内饰
test_boat_entity_survived_export 确定性与导出
test_clean_directory_generation_is_reproducible 确定性与导出

# Verify the saved cottage's geometry, circulation, decoration support and deterministic export. from collections import deque from pathlib import Path import json import subprocess import sys import tempfile import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG, NBTFile, load_schematic AIR = {'air', 'cave_air', 'void_air'} DIRECTIONS = {'north': (0, 0, -1), 'south': (0, 0, 1), 'west': (-1, 0, 0), 'east': (1, 0, 0)} FLOWERS = {'oxeye_daisy', 'cornflower', 'azure_bluet', 'short_grass', 'tall_grass', 'sweet_berry_bush', 'azalea'} LIGHTS = {'lantern', 'candle', 'campfire', 'torch'} def parse(state): """Split an exported state into block name and properties.""" name, _, suffix = state.partition('[') props = dict(p.split('=') for p in suffix.rstrip(']').split(',')) if suffix else {} return name.removeprefix('minecraft:'), props class Saved: """Offer coordinate access to blocks reloaded from the deliverable.""" def __init__(self, path): self.raw = NBTFile.load_regardless_of_gzipped(path) loaded = load_schematic(path) self.ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) self.palette = [parse(s) for s in loaded.palette] self.sy, self.sz, self.sx = loaded.size_yzx def at(self, x, y, z): if not (0 <= x < self.sx and 0 <= y < self.sy and 0 <= z < self.sz): return 'air', {} return self.palette[int(self.ids[y, z, x])] def cells(self): for y in range(self.sy): for z in range(self.sz): for x in range(self.sx): yield (x, y, z), self.at(x, y, z) @pytest.fixture(scope='module') def saved(): return Saved(ROOT / 'output.litematic') def soft(name): """Identify blocks that do not block a person's route.""" return name in AIR or name.endswith('_carpet') or name.endswith('_door') or name in FLOWERS def full_top(name, props): """Check solid support, including top slabs and stair upper surfaces.""" if name in AIR | FLOWERS | LIGHTS | {'water'} or name.startswith('potted_') or name.endswith(('_carpet', '_door', '_trapdoor', '_bed')): return False if name.endswith('_slab'): return props.get('type') in {'top', 'double'} return True def test_single_region_version_size_and_metadata(saved): assert len(saved.raw['Regions']) == 1 assert int(saved.raw['MinecraftDataVersion']) == 3955 assert (saved.sx, saved.sy, saved.sz) == (32, 24, 26) metadata = saved.raw['Metadata'] assert int(metadata['TimeCreated']) == int(metadata['TimeModified']) == 0 assert sum(n != 'air' for _, (n, _) in saved.cells()) > 6500 def test_all_states_are_vanilla_1211(saved): registry = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for name, props in saved.palette: assert name in registry, name for key, value in props.items(): assert key in registry[name][0], (name, key) assert value in registry[name][0][key], (name, key, value) def test_matching_complete_doors(saved): lowers = [] for (x, y, z), (name, props) in saved.cells(): if not name.endswith('_door'): continue dy = 1 if props['half'] == 'lower' else -1 mate_name, mate = saved.at(x, y + dy, z) assert mate_name == name assert mate['half'] != props['half'] for key in ('facing', 'hinge', 'open', 'powered'): assert mate[key] == props[key] if props['half'] == 'lower': lowers.append((x, y, z)) assert full_top(*saved.at(x, y - 1, z)) for dz in (-1, 1): assert soft(saved.at(x, y, z + dz)[0]), (x, y, z, dz) assert soft(saved.at(x, y + 1, z + dz)[0]), (x, y, z, dz) assert set(lowers) == {(12, 5, 9), (19, 10, 12), (20, 10, 12)} def test_complete_beds(saved): count = 0 for (x, y, z), (name, props) in saved.cells(): if not name.endswith('_bed'): continue count += 1 dx, _, dz = DIRECTIONS[props['facing']] sign = 1 if props['part'] == 'foot' else -1 mate_name, mate = saved.at(x + dx * sign, y, z + dz * sign) assert mate_name == name and mate['part'] != props['part'] assert mate['facing'] == props['facing'] assert full_top(*saved.at(x, y - 1, z)) assert count == 2 def walking_graph(saved): """Find supported standing cells with two full blocks of clear headroom. Door blocks are operable. One-block transitions are ordinary stair rises or walkable jumps, with the swept head cell checked on every ascent/descent. Furniture, fences, lanterns and glazing are conservatively impassable. """ nodes = set() for (x, y, z), (name, _) in saved.cells(): if y == 0 or y + 1 >= saved.sy or not soft(name) or not soft(saved.at(x, y + 1, z)[0]): continue below, props = saved.at(x, y - 1, z) if full_top(below, props) and not below.endswith('_fence'): nodes.add((x, y, z)) seen = {(19, 10, 13)} assert seen <= nodes todo = deque(seen) while todo: x, y, z = todo.popleft() for dx, _, dz in DIRECTIONS.values(): for dy in (-1, 0, 1): other = (x + dx, y + dy, z + dz) if other not in nodes or other in seen: continue if dy == 1 and not soft(saved.at(x, y + 2, z)[0]): continue if dy == -1 and not soft(saved.at(x + dx, y + 1, z + dz)[0]): continue seen.add(other) todo.append(other) return nodes, seen @pytest.fixture(scope='module') def routes(saved): return walking_graph(saved) @pytest.mark.parametrize('name,point', [ ('entrance', (19, 10, 13)), ('living', (11, 10, 17)), ('bedside', (11, 10, 10)), ('kitchen', (20, 10, 20)), ('dining', (21, 10, 17)), ('upper_stair_landing', (25, 10, 19)), ('cellar_stair_landing', (25, 5, 13)), ('workshop', (10, 5, 20)), ('storage', (20, 5, 19)), ('lower_entrance', (12, 5, 10)), ('front_path', (19, 3, 4)), ('jetty', (13, 3, 2)), ]) def test_every_room_and_outside_approach_reachable(routes, name, point): assert point in routes[1], (name, point) def test_every_usable_floor_cell_reachable(saved, routes): nodes, reachable = routes floor_cells = {p for p in nodes if p[1] in (5, 10) and ((5 <= p[0] <= 26 and 13 <= p[2] <= 21) or (10 <= p[0] <= 15 and 10 <= p[2] <= 12))} assert len(floor_cells) > 260 assert not floor_cells - reachable, sorted(floor_cells - reachable) def test_stairs_have_correct_rise_and_clearance(saved): for x in (25, 26): for z in range(14, 19): y = z - 9 n, p = saved.at(x, y, z) assert n == 'spruce_stairs' and p['facing'] == 'south' and p['half'] == 'bottom' assert all(soft(saved.at(x, k, z)[0]) for k in (y + 1, y + 2)) for x in (19, 20): for z in range(5, 12): y = z - 2 assert saved.at(x, y, z)[0] == 'brick_stairs' assert all(soft(saved.at(x, k, z)[0]) for k in (y + 1, y + 2)) def test_envelope_separates_all_inside_air_from_sky(saved): # Permeable furnishings and partial blocks cannot conceal a missing wall. def permeable(p): n, _ = saved.at(*p) return n in AIR | FLOWERS | LIGHTS | {'water'} or n.startswith('potted_') or n.endswith(('_carpet', '_fence', '_stairs', '_slab', '_trapdoor', '_bed')) outside = set() todo = deque() for (x, y, z), _ in saved.cells(): if (x in (0, saved.sx - 1) or y in (0, saved.sy - 1) or z in (0, saved.sz - 1)) and permeable((x, y, z)): outside.add((x, y, z)) todo.append((x, y, z)) while todo: x, y, z = todo.popleft() for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)): p = (x + dx, y + dy, z + dz) if p in outside or not (0 <= p[0] < saved.sx and 0 <= p[1] < saved.sy and 0 <= p[2] < saved.sz): continue if permeable(p): outside.add(p) todo.append(p) inside_air = set() for p, (n, _) in saved.cells(): x, y, z = p main = 5 <= x <= 26 and 13 <= z <= 21 and 5 <= y < 14 + min(z - 10, 24 - z) - 1 bay = 10 <= x <= 15 and 10 <= z <= 12 and 5 <= y < 14 + min(x - 8, 17 - x) - 1 if (main or bay) and n in AIR: inside_air.add(p) assert len(inside_air) > 1000 assert not inside_air & outside, sorted(inside_air & outside)[:30] def test_windows_are_clear_and_have_views(saved): front = [(x, y, 12) for x in (6, 7, 24, 25) for y in (6, 10, 11, 12)] front += [(x, y, 9) for x in (12, 13) for y in range(10, 16)] rear = [(x, y, 22) for x in (6, 7, 8, 18, 19, 20) for y in range(10, 13)] for x, y, z in front + rear: assert saved.at(x, y, z)[0] == 'glass', (x, y, z) dz = -1 if (x, y, z) in front else 1 assert saved.at(x, y, z + dz)[0] in AIR, ('outside window', x, y, z) # Kitchen counters are below the sightline, on the sill's first block. if y > 10 or z != 22: assert soft(saved.at(x, y, z - dz)[0]), ('inside window', x, y, z) def test_decorations_are_supported(saved): checked = 0 for (x, y, z), (n, p) in saved.cells(): below = saved.at(x, y - 1, z) if n in FLOWERS: checked += 1 if n == 'tall_grass' and p['half'] == 'upper': assert below == ('tall_grass', {'half': 'lower'}) else: assert below[0] in {'grass_block', 'dirt', 'rooted_dirt', 'coarse_dirt', 'moss_block'}, (n, x, y, z, below) elif n.startswith('potted_') or n in {'candle', 'campfire'} or n.endswith('_carpet'): checked += 1 assert full_top(*below), (n, x, y, z, below) elif n == 'lantern': checked += 1 supporting = saved.at(x, y + 1, z) if p.get('hanging') == 'true' else below assert full_top(*supporting), (n, x, y, z, supporting) elif n.endswith('_trapdoor'): checked += 1 # A shutter or planter edge must touch its wall, soil or shelf. attached = any(full_top(*saved.at(x + dx, y, z + dz)) for dx, _, dz in DIRECTIONS.values()) assert attached or full_top(*below), (n, x, y, z) assert checked > 150 def test_room_lights_near_the_floor(saved): rooms = { 'living': ((5, 10, 13), (11, 13, 21)), 'bay': ((10, 10, 10), (15, 13, 12)), 'hall': ((17, 10, 13), (23, 13, 16)), 'dining_kitchen': ((17, 10, 17), (23, 13, 21)), 'workshop': ((5, 5, 13), (15, 8, 21)), 'cellar_bay': ((10, 5, 10), (15, 8, 12)), 'storage': ((17, 5, 13), (26, 8, 21)), } for name, (lo, hi) in rooms.items(): lights = [p for p, (n, props) in saved.cells() if n in LIGHTS and props.get('lit', 'true') == 'true' and all(lo[i] <= p[i] <= hi[i] for i in range(3))] assert lights, name def test_required_materials_and_furniture(saved): counts = {} for _, (n, _) in saved.cells(): counts[n] = counts.get(n, 0) + 1 for name, minimum in {'smooth_sandstone_stairs': 200, 'dark_oak_stairs': 45, 'dark_oak_log': 40, 'brick_stairs': 24, 'smooth_sandstone_slab': 10, 'barrel': 12, 'chest': 4, 'candle': 3, 'lantern': 12, 'glass': 60, 'water': 80}.items(): assert counts.get(name, 0) >= minimum, (name, counts.get(name, 0)) assert counts.get('spruce_planks', 0) and counts.get('oak_planks', 0) assert counts.get('smooth_sandstone', 0) and counts.get('sandstone', 0) and counts.get('cut_sandstone', 0) def test_boat_entity_survived_export(saved): region = next(iter(saved.raw['Regions'].values())) entities = region['Entities'] assert len(entities) == 1 boat = entities[0] assert str(boat['id']) == 'minecraft:boat' and str(boat['Type']) == 'spruce' x, y, z = map(float, boat['Pos']) assert saved.at(int(x), 1, int(z))[0] == 'water' assert 1 < y < 2 and len(boat['UUID']) == 4 def test_clean_directory_generation_is_reproducible(saved): with tempfile.TemporaryDirectory(dir=ROOT / 'test') as temp: directory = Path(temp) subprocess.run([sys.executable, str(ROOT / 'generate.py')], cwd=directory, check=True, capture_output=True) first = (directory / 'output.litematic').read_bytes() subprocess.run([sys.executable, str(ROOT / 'generate.py')], cwd=directory, check=True, capture_output=True) assert first == (directory / 'output.litematic').read_bytes() assert first == (ROOT / 'output.litematic').read_bytes()
stone-cottage — 11 个测试 / 331 行 / test_cottage.py

test_java_version_one_region_scale_and_palette 尺寸与比例
test_complete_door_and_recess 可达性与流线
test_complete_supported_bed 支撑与悬空
test_main_room_and_loft_reachable_with_headroom 可达性与流线
test_closed_envelope_and_solid_roof_backing 封闭性与外壳
test_clear_windows_and_continuous_wood_frame 其它
test_decorations_have_support 支撑与悬空
test_lighting_covers_both_floors 光照
test_material_variation_and_single_gable 材质与状态
test_block_entity_round_trip 其它
test_clean_directory_rerun_is_byte_identical 确定性与导出

# Validate the exported cottage geometry, traversal, attachments, NBT and reproducibility. from collections import deque from pathlib import Path import shutil import subprocess import sys from tempfile import TemporaryDirectory import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import BLOCK_REGISTRY, Build, CONFIG from mcio.nbt.nbt import File as NBTFile from mcio.schematic import load_schematic DIRECTIONS = {"north": (0, 0, -1), "south": (0, 0, 1), "east": (1, 0, 0), "west": (-1, 0, 0)} NEIGHBORS = tuple(DIRECTIONS.values()) + ((0, 1, 0), (0, -1, 0)) FULL = {"grass_block", "coarse_dirt", "gravel", "andesite", "stone", "cobblestone", "mossy_cobblestone", "stone_bricks", "stripped_oak_log", "spruce_planks", "oak_planks", "terracotta", "brown_terracotta", "hay_block", "furnace", "smoker", "crafting_table", "barrel", "bookshelf"} PLANTS = {"short_grass", "cornflower", "dandelion", "sunflower"} def add(a, b): """Return the sum of two XYZ coordinates.""" return tuple(x + y for x, y in zip(a, b)) def decode(state): """Split a blockstate into its vanilla name and properties.""" name, _, tail = state.partition("[") return name.removeprefix("minecraft:"), dict(part.split("=") for part in tail.rstrip("]").split(",") if part) class Volume: """Inspect only the reloaded deliverable, never the generator's placement map.""" def __init__(self, path): """Load the schematic and its block entity data from a Path.""" self.loaded = load_schematic(path) self.ids = self.loaded.read_flat(0, self.loaded.volume).reshape(self.loaded.size_yzx) self.size = (self.loaded.size_yzx[2], self.loaded.size_yzx[0], self.loaded.size_yzx[1]) self.nbt = NBTFile.load_regardless_of_gzipped(filename=path) self.cells = {(x, y, z): decode(self.loaded.palette[int(self.ids[y, z, x])]) for x in range(self.size[0]) for y in range(self.size[1]) for z in range(self.size[2])} def block(self, pos): """Read a position, treating out-of-volume coordinates as air.""" return self.cells.get(pos, ("air", {})) def full(self, pos): """Identify a full cube or a stair with a complete upper surface.""" name, props = self.block(pos) return name in FULL or (name.endswith("_stairs") and props["half"] == "top") def body_clear(self, pos): """Allow air, ladders and an operable wooden door on walking paths.""" name, _ = self.block(pos) return name in ("air", "ladder", "spruce_door") def walkable(self, pos): """Require two clear body cells and a floor or climbable ladder.""" below = add(pos, (0, -1, 0)) floor_name, _ = self.block(below) supported = self.full(below) or floor_name.endswith("_stairs") or self.block(pos)[0] == "ladder" return supported and self.body_clear(pos) and self.body_clear(add(pos, (0, 1, 0))) def reachable(self): """Flood walking and ladder moves from the foot of the front steps.""" start = (7, 1, 0) assert self.walkable(start) seen, queue = {start}, deque([start]) while queue: pos = queue.popleft() for dx, _, dz in DIRECTIONS.values(): for dy in (-1, 0, 1): dest = add(pos, (dx, dy, dz)) # A step up also requires head clearance over the starting tile. if dy == 1 and not self.body_clear(add(pos, (0, 2, 0))): continue if dy == -1 and not self.body_clear(add(dest, (0, 2, 0))): continue if dest in self.cells and dest not in seen and self.walkable(dest): seen.add(dest) queue.append(dest) for dy in (-1, 1): dest = add(pos, (0, dy, 0)) if self.block(pos)[0] == "ladder" and self.block(dest)[0] == "ladder" and dest not in seen and self.walkable(dest): seen.add(dest) queue.append(dest) return seen @pytest.fixture(scope="module") def volume(): """Load the exact output.litematic contract path.""" return Volume(ROOT / "output.litematic") def test_java_version_one_region_scale_and_palette(volume): """Check format, scale and every property against the Java 1.21.1 registry.""" assert int(volume.nbt["MinecraftDataVersion"]) == 3955 assert len(volume.nbt["Regions"]) == 1 assert volume.size == (16, 15, 14) assert max(p[1] for p, (name, _) in volume.cells.items() if name != "air") == 13 for state in volume.loaded.palette: name, props = decode(state) assert name in BLOCK_REGISTRY allowed, defaults = BLOCK_REGISTRY[name] assert props.keys() == defaults.keys(), state assert all(value in allowed[key] for key, value in props.items()), state def test_complete_door_and_recess(volume): """Require matched door halves, a real recessed threshold and solid jambs.""" doors = [(p, props) for p, (name, props) in volume.cells.items() if name.endswith("_door")] assert len(doors) == 2 lower = volume.block((7, 2, 4)) upper = volume.block((7, 3, 4)) assert lower[1]["half"] == "lower" and upper[1]["half"] == "upper" assert {k: v for k, v in lower[1].items() if k != "half"} == {k: v for k, v in upper[1].items() if k != "half"} assert lower[1]["facing"] == "north" and lower[1]["open"] == "false" for y in (2, 3): assert volume.block((7, y, 3))[0] == "air" assert volume.full((6, y, 4)) and volume.full((8, y, 4)) assert volume.full((7, 4, 4)) and volume.full((7, 1, 4)) assert all((7, 2, z) in volume.reachable() for z in range(2, 6)) def test_complete_supported_bed(volume): """Check bed parts, facing, adjacency, support and clear space above.""" beds = [(p, name, props) for p, (name, props) in volume.cells.items() if name.endswith("_bed")] assert len(beds) == 2 for p, name, props in beds: direction = DIRECTIONS[props["facing"]] partner = add(p, direction if props["part"] == "foot" else tuple(-v for v in direction)) partner_name, partner_props = volume.block(partner) assert partner_name == name assert partner_props["part"] != props["part"] assert partner_props["facing"] == props["facing"] assert volume.full(add(p, (0, -1, 0))) assert volume.block(add(p, (0, 1, 0)))[0] == "air" def test_main_room_and_loft_reachable_with_headroom(volume): """Check every free floor tile, the ladder and access beside each furnishing.""" reached = volume.reachable() required = set() for x in range(4, 11): for z in range(4, 11): p = (x, 2, z) if volume.body_clear(p): assert volume.body_clear((x, 3, z)), f"low main-room ceiling: {p}" required.add(p) for x in range(5, 10): for z in range(8, 11): p = (x, 7, z) if volume.body_clear(p): assert volume.body_clear((x, 8, z)), f"low loft ceiling: {p}" required.add(p) assert required <= reached, f"unreachable floor tiles: {required - reached}" assert all((7, y, 10) in reached for y in range(2, 8)) furniture = {"furnace", "smoker", "crafting_table", "barrel", "bookshelf", "red_bed"} for p, (name, _) in volume.cells.items(): if name in furniture: assert any(add(p, d) in reached for d in DIRECTIONS.values()), f"inaccessible furnishing: {p} {name}" assert (8, 2, 6) in reached, "table lacks a clear approach" def test_closed_envelope_and_solid_roof_backing(volume): """Flood outside air, conservatively allowing passage through all roof stairs.""" def sealed(p): name, props = volume.block(p) return name in FULL or name == "glass_pane" or (name.endswith("_door") and props["open"] == "false") boundary = {p for p in volume.cells if any(v in (0, volume.size[i] - 1) for i, v in enumerate(p)) and not sealed(p)} outside, queue = set(boundary), deque(boundary) while queue: p = queue.popleft() for delta in NEIGHBORS: dest = add(p, delta) if dest in volume.cells and dest not in outside and not sealed(dest): outside.add(dest) queue.append(dest) interior_air = {(x, y, z) for x in range(4, 11) for z in range(4, 11) for y in range(2, 11 - abs(x - 7)) if volume.block((x, y, z))[0] == "air"} assert len(interior_air) > 200 assert not interior_air & outside, f"sky-connected interior air: {interior_air & outside}" for x in range(2, 13): for z in range(2, 13): assert volume.full((x, 11 - abs(x - 7), z)), f"missing solid roof backing: {(x, z)}" def test_clear_windows_and_continuous_wood_frame(volume): """Keep both openings glazed and clear inside, with complete timber frames.""" panes = [p for p, (name, _) in volume.cells.items() if name == "glass_pane"] assert len(panes) == 12 for x in (3, 11): inside_x = x + (1 if x == 3 else -1) outside_x = x + (-1 if x == 3 else 1) for z in range(6, 9): for y in (3, 4): name, props = volume.block((x, y, z)) assert name == "glass_pane" and props["north"] == props["south"] == "true" assert props["east"] == props["west"] == "false" assert volume.block((inside_x, y, z))[0] == "air" assert volume.block((outside_x, y, z))[0] in ("air", "sunflower") for y in (2, 5): assert volume.block((x, y, z))[0] == "stripped_oak_log" for z in (5, 9): assert all(volume.block((x, y, z))[0] == "stripped_oak_log" for y in range(2, 6)) for z in (3, 11): assert all(volume.block((x, y, z))[0] == "stripped_oak_log" for y in range(1, 7)) def test_decorations_have_support(volume): """Check attachment directions, hanging chains, planters, plants and furniture.""" for p, (name, props) in volume.cells.items(): below, above = add(p, (0, -1, 0)), add(p, (0, 1, 0)) if name == "lantern": anchor = above if props["hanging"] == "true" else below while volume.block(anchor)[0] == "chain": assert volume.block(anchor)[1]["axis"] == "y" anchor = add(anchor, (0, 1, 0)) assert volume.full(anchor), f"unsupported lantern: {p}" elif name == "chain": anchor = above while volume.block(anchor)[0] == "chain": anchor = add(anchor, (0, 1, 0)) assert volume.full(anchor), f"unsupported chain: {p}" elif name == "ladder": assert volume.full(add(p, tuple(-v for v in DIRECTIONS[props["facing"]]))), p elif name == "vine": attachments = [add(p, d) for face, d in DIRECTIONS.items() if props[face] == "true"] assert attachments and all(volume.full(a) for a in attachments), p elif name in PLANTS: if name == "sunflower" and props["half"] == "upper": assert volume.block(below) == ("sunflower", {"half": "lower"}) else: assert volume.block(below)[0] in ("grass_block", "dirt"), p if name == "sunflower": assert volume.block(above) == ("sunflower", {"half": "upper"}) elif name.startswith("potted_") or name in ("hay_block", "barrel", "bookshelf", "crafting_table", "furnace", "smoker"): assert volume.full(below), (p, name) elif name.endswith("_fence"): assert volume.full(below) or volume.block(below)[0].endswith("_fence"), p elif name.endswith("_trapdoor"): assert props["open"] == "true" assert any(volume.full(add(p, d)) for d in DIRECTIONS.values()), p elif name.endswith("_leaves"): assert props["persistent"] == "true" pending, seen, anchored = [p], {p}, False while pending: current = pending.pop() for delta in NEIGHBORS: q = add(current, delta) if volume.full(q): anchored = True elif volume.block(q)[0].endswith("_leaves") and q not in seen: seen.add(q) pending.append(q) assert anchored, f"floating bush: {p}" def test_lighting_covers_both_floors(volume): """Propagate lantern light through air to check all reachable interior tiles.""" light = {} queue = deque() for p, (name, _) in volume.cells.items(): if name == "lantern": light[p] = 15 queue.append(p) while queue: p = queue.popleft() level = light[p] - 1 for d in NEIGHBORS: q = add(p, d) name, _ = volume.block(q) if q in volume.cells and name not in FULL and level > light.get(q, 0): light[q] = level queue.append(q) for p in volume.reachable(): x, y, z = p if 4 <= x <= 10 and 4 <= z <= 10 and y in (2, 7): assert light.get(p, 0) >= 7, f"dark floor tile: {p}" assert any(volume.block((x, y, z))[0] == "lantern" for x in range(5, 10) for y in range(7, 10) for z in range(8, 11)) def test_material_variation_and_single_gable(volume): """Require related stone textures, low moss and one plain pitched roof.""" wall_names = {volume.block((x, y, 11))[0] for x in range(4, 11) for y in range(2, 6)} assert {"stone", "andesite", "cobblestone"} <= wall_names assert any(name == "mossy_cobblestone" and p[1] <= 2 for p, (name, _) in volume.cells.items()) roof_names = set() for x in range(2, 13): height = 12 - abs(x - 7) for z in range(4, 12): name, props = volume.block((x, height, z)) if x == 7: assert name == "andesite_slab" else: assert name in ("andesite_stairs", "cobblestone_stairs", "stone_stairs") assert props["facing"] == ("east" if x < 7 else "west") roof_names.add(name) assert len(roof_names) == 3 def test_block_entity_round_trip(volume): """Check bed entities and Java 1.21 item counts in the four storage barrels.""" region = next(iter(volume.nbt["Regions"].values())) entities = {(int(t["x"]), int(t["y"]), int(t["z"])): t for t in region["TileEntities"]} expected = {p for p, (name, _) in volume.cells.items() if name in ("red_bed", "barrel", "furnace", "smoker")} assert set(entities) == expected for p, tag in entities.items(): name = volume.block(p)[0] assert str(tag["id"]) == "minecraft:" + ("bed" if name == "red_bed" else name) if name == "barrel": assert len(tag["Items"]) == 1 item = tag["Items"][0] assert str(item["id"]).startswith("minecraft:") assert 1 <= int(item["count"]) <= 64 and int(item["Slot"]) == 0 def test_clean_directory_rerun_is_byte_identical(): """Rerun a standalone copied generator with no arguments and compare bytes.""" with TemporaryDirectory(prefix="cottage-check-", dir=ROOT / "test") as temp: clean = Path(temp) shutil.copy2(ROOT / "generate.py", clean / "generate.py") result = subprocess.run([sys.executable, "generate.py"], cwd=clean, check=True, capture_output=True, text=True) assert "wrote output.litematic" in result.stdout assert (clean / "output.litematic").read_bytes() == (ROOT / "output.litematic").read_bytes() other = Build().build().export(clean / "second.litematic") assert other.read_bytes() == (ROOT / "output.litematic").read_bytes()
red-roof-farmstead — 10 个测试 / 346 行 / test_farmstead.py

test_single_region_version_and_scale 尺寸与比例
test_all_blockstates_exist_in_java_1211 材质与状态
test_export_round_trip_and_clean_directory_reproducibility 确定性与导出
test_doors_beds_and_double_plants_are_complete 可达性与流线
test_every_floor_and_work_area_is_reachable 可达性与流线
test_interior_envelope_has_no_connection_to_sky 封闭性与外壳
test_windows_and_doorway_have_clear_openings 可达性与流线
test_decorations_have_support_and_structure_is_anchored 支撑与悬空
test_each_room_has_nearby_floor_lighting 光照
test_farm_features_and_four_sails 其它

# Validate the delivered farmstead's geometry, supported details, and reproducibility. from __future__ import annotations from collections import deque import gzip import json from pathlib import Path import subprocess import sys import tempfile import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG from mcio.nbt.nbt import File as NBTFile from mcio.schematic import load_schematic DIRECTIONS = [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)] FACING = {'north': (0, 0, -1), 'south': (0, 0, 1), 'west': (-1, 0, 0), 'east': (1, 0, 0)} PLANTS = {'short_grass', 'tall_grass', 'poppy', 'oxeye_daisy', 'cornflower', 'dandelion', 'wheat'} AIR = {'air', 'cave_air', 'void_air'} def split_state(state): """Split a palette string into its vanilla name and property mapping.""" name, _, tail = state.partition('[') properties = dict(pair.split('=') for pair in tail.rstrip(']').split(',') if pair) return name.removeprefix('minecraft:'), properties class World: """Expose reloaded schematic blocks in XYZ order for independent checks.""" def __init__(self, path): self.loaded = load_schematic(path) self.ids = self.loaded.read_flat(0, self.loaded.volume).reshape(self.loaded.size_yzx) self.sy, self.sz, self.sx = self.ids.shape self.palette = [split_state(s) for s in self.loaded.palette] def at(self, p): x, y, z = p if not (0 <= x < self.sx and 0 <= y < self.sy and 0 <= z < self.sz): return 'air', {} return self.palette[int(self.ids[y, z, x])] def name(self, p): return self.at(p)[0] def blocks(self): for y, z, x in np.argwhere(self.ids != 0): p = (int(x), int(y), int(z)) name, properties = self.at(p) if name not in AIR: yield p, name, properties @pytest.fixture(scope='module') def world(): """Load the actual deliverable, independently of generator memory.""" return World(ROOT / 'output.litematic') def add(a, b): """Add two XYZ coordinates.""" return tuple(x + y for x, y in zip(a, b)) def foot_clear(world, p): """Allow air, thin floor covers, climbable ladders, and operable doorways.""" name, props = world.at(p) return name in AIR | PLANTS | {'ladder'} or name.endswith('_carpet') or name.endswith('_door') or (name.endswith('_fence_gate') and props.get('open') == 'true') def head_clear(world, p): """Require unobstructed headroom, allowing only ladders and operable doors.""" name = world.name(p) return name in AIR | PLANTS | {'ladder'} or name.endswith('_door') def supports_feet(world, p): """Recognize grounded walking surfaces, excluding furniture and narrow posts.""" name = world.name(p) return not (name in AIR | PLANTS | {'water', 'ladder', 'lantern', 'chain', 'flower_pot', 'candle'} or name.endswith(('_fence', '_door', '_fence_gate', '_carpet', '_bed'))) @pytest.fixture(scope='module') def reachable(world): """Flood walking positions, including one-block steps and backed ladder climbs.""" nodes = set() for x in range(1, 41): for z in range(1, 42): for y in range(3, 18): p = (x, y, z) if foot_clear(world, p) and head_clear(world, (x, y + 1, z)): if supports_feet(world, (x, y - 1, z)) or world.name(p) == 'ladder': nodes.add(p) start = (19, 3, 41) assert start in nodes seen = {start} queue = deque([start]) while queue: p = queue.popleft() x, y, z = p neighbors = [] for dx, _, dz in FACING.values(): for dy in (-1, 0, 1): q = (x + dx, y + dy, z + dz) if q not in nodes: continue # Ascending needs clearance above the previous head position. high_column = (x, y + 2, z) if dy == 1 else (q[0], q[1] + 2, q[2]) if dy == 0 or head_clear(world, high_column): neighbors.append(q) for dy in (-1, 1): q = (x, y + dy, z) if q in nodes and (world.name(p) == 'ladder' or world.name(q) == 'ladder'): neighbors.append(q) for q in neighbors: if q not in seen: seen.add(q) queue.append(q) return seen def test_single_region_version_and_scale(world): """Require the contractual file, correct data version, and reference-sized plot.""" with gzip.open(ROOT / 'output.litematic', 'rb') as stream: nbt = NBTFile.from_fileobj(stream) assert int(nbt['MinecraftDataVersion']) == 3955 assert len(nbt['Regions']) == 1 assert (world.sx, world.sz) == (42, 42) assert int(nbt['Metadata']['TimeCreated']) == 0 assert int(nbt['Metadata']['TimeModified']) == 0 assert all(world.name((x, 0, z)) not in AIR for x in range(42) for z in range(42)) assert all(world.name((x, 3, z)) not in AIR for x in range(9, 29) for z in range(7, 23)) assert world.name((18, 25, 14)) == 'red_nether_brick_slab' def test_all_blockstates_exist_in_java_1211(world): """Validate every used name, property, and value against installed vanilla data.""" valid = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for name, properties in world.palette: assert name in valid, name schema, _ = valid[name] for key, value in properties.items(): assert key in schema and value in schema[key], (name, key, value) def test_export_round_trip_and_clean_directory_reproducibility(world): """Regenerate with no arguments in a clean temporary directory and compare bytes.""" build = Build().build() build.verify(ROOT / 'output.litematic') with tempfile.TemporaryDirectory(prefix='farm-', dir=ROOT / 'test') as directory: scratch = Path(directory) result = subprocess.run([sys.executable, str(ROOT / 'generate.py')], cwd=scratch, capture_output=True, text=True, check=True) assert 'wrote output.litematic' in result.stdout assert (scratch / 'output.litematic').read_bytes() == (ROOT / 'output.litematic').read_bytes() def test_doors_beds_and_double_plants_are_complete(world): """Check every paired block, facing, support, and unobstructed door passage.""" door_count = bed_count = 0 for p, name, props in world.blocks(): x, y, z = p if name.endswith('_door'): other = (x, y + (1 if props['half'] == 'lower' else -1), z) other_name, other_props = world.at(other) assert other_name == name assert other_props['half'] != props['half'] assert all(other_props[k] == props[k] for k in ['facing', 'hinge', 'open', 'powered']) assert foot_clear(world, (x, y, z - 1)) and foot_clear(world, (x, y, z + 1)) door_count += 1 if name.endswith('_bed'): step = FACING[props['facing']] if props['part'] == 'head': step = tuple(-v for v in step) other_name, other_props = world.at(add(p, step)) assert other_name == name assert other_props['part'] != props['part'] assert other_props['facing'] == props['facing'] assert supports_feet(world, (x, y - 1, z)) assert world.name((x, y + 1, z)) == 'air' bed_count += 1 if name == 'tall_grass': other_name, other_props = world.at((x, y + (1 if props['half'] == 'lower' else -1), z)) assert other_name == name and other_props['half'] != props['half'] assert door_count == 4 assert bed_count == 6 def test_every_floor_and_work_area_is_reachable(world, reachable): """Reach both floors, the dormer, table, hearth, beds, well, and field on foot.""" targets = [(18, 4, 22), (19, 4, 21), (23, 4, 19), (12, 4, 9), (15, 4, 14), (11, 4, 14), (10, 10, 14), (14, 10, 14), (19, 10, 21), (25, 10, 13), (6, 3, 34), (26, 3, 31), (35, 3, 19)] for p in targets: assert p in reachable, ('destination unreachable', p) for y in (4, 10): required = [] for x in range(10, 28): for z in range(8, 22): p = (x, y, z) if foot_clear(world, p) and head_clear(world, (x, y + 1, z)) and supports_feet(world, (x, y - 1, z)): required.append(p) assert len(required) >= 120 assert set(required) <= reachable, ('isolated floor cells', set(required) - reachable) for y in range(4, 10): assert world.at((10, y, 14)) == ('ladder', {'facing': 'east'}) assert supports_feet(world, (9, y, 14)) def test_interior_envelope_has_no_connection_to_sky(world): """Flood exterior air conservatively through partial blocks; no room may leak.""" def leaks(p): name, _ = world.at(p) return name in AIR | PLANTS | {'ladder', 'chain', 'lantern', 'candle', 'flower_pot', 'iron_bars', 'campfire'} or name.endswith(('_stairs', '_slab', '_fence', '_carpet', '_bed', '_fence_gate')) sky = {(x, world.sy - 1, z) for x in range(world.sx) for z in range(world.sz)} queue = deque(sky) while queue: p = queue.popleft() for delta in DIRECTIONS: q = add(p, delta) x, y, z = q if 0 <= x < world.sx and 0 <= y < world.sy and 0 <= z < world.sz and q not in sky and leaks(q): sky.add(q) queue.append(q) interior = set() for x in range(10, 28): for z in range(8, 22): # Independent expected roof profile: eave backing starts at y=8. top = 7 + min(z - 5, 24 - z) for y in range(4, top + 1): if world.name((x, y, z)) in AIR: interior.add((x, y, z)) for x in range(16, 22): for z in range(18, 23): for y in range(10, 11 + min(x - 14, 23 - x)): if world.name((x, y, z)) in AIR: interior.add((x, y, z)) assert len(interior) > 1500 assert not interior & sky, ('weather leak', sorted(interior & sky)[:20]) def test_windows_and_doorway_have_clear_openings(world): """Ensure glazing remains unobstructed inside and outside every visible window.""" windows = [] for z in (7, 22): for x0 in (11, 25): for x in (x0, x0 + 1): for y in range(5, 8): windows.append(((x, y, z), (0, 0, 1))) for x in (9, 28): for z0 in (10, 16): for z in (z0, z0 + 1): for y in range(5, 8): windows.append(((x, y, z), (1, 0, 0))) for z in (14, 15): for y in range(12, 15): windows.append(((x, y, z), (1, 0, 0))) for x in range(17, 21): for y in range(11, 14): windows.append(((x, y, 23), (0, 0, 1))) for p, normal in windows: assert world.name(p) == 'gray_stained_glass', p for sign in (-1, 1): neighbor = add(p, tuple(v * sign for v in normal)) assert world.name(neighbor) in AIR | {'candle', 'flower_pot'}, ('window obstruction', p, neighbor, world.name(neighbor)) def test_decorations_have_support_and_structure_is_anchored(world): """Check fragile blocks individually and every solid assembly back to the ground.""" attached = set() for p, name, props in world.blocks(): if name in AIR | PLANTS | {'water'}: continue attached.add(p) x, y, z = p below = (x, y - 1, z) if name in PLANTS - {'tall_grass'}: assert world.name(below) in {'grass_block', 'farmland'} if name == 'lantern': support = (x, y + 1, z) if props.get('hanging') == 'true' else below assert world.name(support) not in AIR | PLANTS | {'water'}, (p, support) if name == 'chain': assert world.name((x, y + 1, z)) not in AIR | PLANTS | {'water'} if name in {'candle', 'flower_pot', 'campfire', 'grindstone'} or name.endswith('_carpet'): assert supports_feet(world, below), (p, name, world.name(below)) lower_name, lower_props = world.at(below) if lower_name.endswith('_slab'): assert lower_props.get('type') in {'top', 'double'} if name == 'ladder': normal = FACING[props['facing']] support = add(p, tuple(-v for v in normal)) assert supports_feet(world, support) roots = {p for p in attached if p[1] == 0} seen = set(roots) queue = deque(roots) while queue: p = queue.popleft() for delta in DIRECTIONS: q = add(p, delta) if q in attached and q not in seen: seen.add(q) queue.append(q) assert attached <= seen, ('floating assembly', attached - seen) for p, name, props in world.blocks(): if name in PLANTS: x, y, z = p support = world.name((x, y - 1, z)) assert support in ({'tall_grass'} if props.get('half') == 'upper' else {'grass_block', 'farmland'}), (p, name, support) def test_each_room_has_nearby_floor_lighting(world, reachable): """Require local light sources across both floors, not just distant roof lights.""" lights = [] for p, name, props in world.blocks(): if name == 'lantern' or (name in {'campfire', 'candle', 'furnace'} and props.get('lit') == 'true'): lights.append(p) for y in (4, 10): local = [p for p in lights if 10 <= p[0] <= 27 and 8 <= p[2] <= 22 and y <= p[1] <= y + 3] assert len(local) >= 4 for p in reachable: if p[1] == y and 10 <= p[0] <= 27 and 8 <= p[2] <= 21: assert min(sum((a - b)**2 for a, b in zip(p, q)) for q in local) <= 49, ('unlit floor area', p) def test_farm_features_and_four_sails(world): """Check wheat hydration, water basin, lit chimney, and four clear mill arms.""" wheat = [p for p, name, _ in world.blocks() if name == 'wheat'] assert len(wheat) >= 60 for x, y, z in wheat: assert world.at((x, y - 1, z)) == ('farmland', {'moisture': '7'}) assert any(world.name((xx, y - 1, zz)) == 'water' for xx in range(x - 4, x + 5) for zz in range(z - 4, z + 5)), (x, z) assert all(world.name((x, 4, z)) == 'water' for x in range(5, 8) for z in range(29, 32)) assert world.at((26, 22, 19))[1]['lit'] == 'true' for d in range(1, 6): for p in [(35 + d, 13, 15), (35 - d, 13, 15), (35, 13 + d, 15), (35, 13 - d, 15)]: assert world.name(p) == 'stripped_oak_log', p sails = [p for p, name, _ in world.blocks() if name == 'oak_fence' and p[2] == 15 and 8 <= p[1] <= 18] assert len(sails) == 24 assert all(world.name((x, 4, z)) in AIR for x in range(29, 34) for z in range(10, 14)) furniture = {name for _, name, _ in world.blocks()} assert {'barrel', 'chest', 'hay_block', 'red_carpet', 'candle', 'spruce_stairs', 'crafting_table', 'red_bed'} <= furniture
stilt-water-cabin — 10 个测试 / 397 行 / test_cabin.py

test_one_region_version_scale_and_valid_vanilla_states 尺寸与比例
test_timber_platform_and_piles_leave_water_visible 支撑与悬空
test_deck_landing_and_every_room_floor_reachable 可达性与流线
test_doors_and_bed_are_complete_matching_pairs 可达性与流线
test_all_room_and_attic_air_is_separated_from_sky 家具与内饰
test_windows_and_door_openings_stay_clear 可达性与流线
test_roof_overhang_and_lighting 封闭性与外壳
test_decorations_have_real_support 支撑与悬空
test_saved_boat_and_stocked_containers 家具与内饰
test_clean_directory_rerun_is_byte_identical 确定性与导出

# Verify the exported cabin geometry, access, attachments, entity data, and determinism. from __future__ import annotations from collections import deque import json from pathlib import Path import shutil import subprocess import sys import tempfile import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import BOAT_POSITION, CONFIG, DECK_POSTS, DOORS, FLOOR_Y, LIGHTS, POSTS, WINDOWS, Build from mcio.nbt.nbt import File as NBTFile from mcio.schematic import load_schematic DIRECTIONS = ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)) HORIZONTAL = ((1, 0), (-1, 0), (0, 1), (0, -1)) FACING = {"east": (1, 0), "west": (-1, 0), "south": (0, 1), "north": (0, -1)} AIR = "minecraft:air" def parse(state): """Split a canonical block state into its name and properties. Args: state (str): Namespaced state string. Returns: tuple: Name and property mapping. """ name, _, props = state.partition("[") return name, dict(p.split("=") for p in props.rstrip("]").split(",") if p) class SavedBuild: """Read geometry and entities independently from the delivered file.""" def __init__(self, path: Path): """Load the palette, block volume, and complete original NBT. Args: path (Path): Delivered schematic. """ self.loaded = load_schematic(path) self.ids = self.loaded.read_flat(0, self.loaded.volume).reshape(self.loaded.size_yzx) self.sy, self.sz, self.sx = self.ids.shape self.nbt = NBTFile.load_regardless_of_gzipped(path) self.region = next(iter(self.nbt["Regions"].values())) self.states = {(x, y, z): self.loaded.palette[int(self.ids[y, z, x])] for y in range(self.sy) for z in range(self.sz) for x in range(self.sx)} def state(self, x, y, z): """Read a cell, using air for coordinates outside the volume. Args: x (int): X coordinate. y (int): Y coordinate. z (int): Z coordinate. Returns: str: Canonical state. """ return self.states.get((x, y, z), AIR) def name(self, x, y, z): """Read only the block name at an XYZ coordinate. Args: x (int): X coordinate. y (int): Y coordinate. z (int): Z coordinate. Returns: str: Namespaced block name. """ return self.state(x, y, z).split("[")[0] def solid(self, x, y, z): """Recognize full construction or furniture cubes in this build. Args: x (int): X coordinate. y (int): Y coordinate. z (int): Z coordinate. Returns: bool: Whether the cell has full supporting cube geometry. """ name = self.name(x, y, z) return name.endswith(("_log", "_planks")) or name in {"minecraft:dirt", "minecraft:mud", "minecraft:rooted_dirt", "minecraft:clay", "minecraft:grass_block", "minecraft:barrel", "minecraft:smoker", "minecraft:crafting_table"} @pytest.fixture(scope="module") def saved(): """Load the actual delivery once for all independent structural checks.""" return SavedBuild(ROOT / "output.litematic") def walkable(saved): """Enumerate floor positions with two blocks of usable headroom. Args: saved (SavedBuild): Delivered geometry. Returns: set: Safe integer standing positions, including stair transitions. """ nodes = set() for (x, y, z), state in saved.states.items(): if not 4 <= y <= 7: continue feet, props = parse(state) head = saved.name(x, y + 1, z) feet_clear = feet == AIR or feet.endswith(("_carpet", "_door")) head_clear = head == AIR or head.endswith("_door") below = saved.name(x, y - 1, z) support = saved.solid(x, y - 1, z) or below.endswith("_stairs") # Carpet raises the player 1/16 block, so check the next cell too. carpet_clear = not feet.endswith("_carpet") or saved.name(x, y + 2, z) == AIR if feet_clear and head_clear and support and carpet_clear: nodes.add((x, y, z)) return nodes def reachable(nodes, start): """Flood supported walking positions with at most one-block steps. Args: nodes (set): Positions with headroom and support. start (tuple): Start position. Returns: set: Reachable positions. """ assert start in nodes visited = {start} queue = deque([start]) while queue: x, y, z = queue.popleft() for dx, dz in HORIZONTAL: for dy in (-1, 0, 1): target = (x + dx, y + dy, z + dz) if target in nodes and target not in visited: visited.add(target) queue.append(target) return visited def test_one_region_version_scale_and_valid_vanilla_states(saved): """Check the contract, scale, and every property against the shipped registry.""" assert int(saved.nbt["MinecraftDataVersion"]) == 3955 assert len(saved.nbt["Regions"]) == 1 assert (saved.sx, saved.sy, saved.sz) == (28, 21, 24) assert int(saved.nbt["Metadata"]["TimeCreated"]) == 0 assert int(saved.nbt["Metadata"]["TimeModified"]) == 0 registry = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for state in saved.loaded.palette: name, props = parse(state) assert name.startswith("minecraft:") allowed, defaults = registry[name.removeprefix("minecraft:")] for key, value in props.items(): assert key in allowed, (state, key) assert value in allowed[key], (state, key, value) assert max(y for (x, y, z), state in saved.states.items() if state != AIR) - 2 in range(14, 18) def test_timber_platform_and_piles_leave_water_visible(saved): """Verify floor continuity, anchored piles, and open flooded space underneath.""" for x in range(9, 24): for z in range(6, 17): assert saved.solid(x, FLOOR_Y, z), (x, z) for x, z in POSTS + DECK_POSTS: for y in range(0, FLOOR_Y + 1): assert saved.solid(x, y, z), (x, y, z) under = [(x, z) for x in range(10, 23) for z in range(7, 16)] assert sum(saved.name(x, 2, z) == "minecraft:water" for x, z in under) / len(under) > 0.95 assert all(saved.name(x, 3, z) == AIR and saved.name(x, 4, z) == AIR for x, z in under) for z in (6, 16): for x in range(9, 24): assert saved.name(x, 5, z) == ("minecraft:oak_log" if (x, z) in POSTS else "minecraft:spruce_log") all_names = {parse(state)[0] for state in saved.loaded.palette} assert {"minecraft:oak_log", "minecraft:spruce_log", "minecraft:stripped_oak_log", "minecraft:stripped_spruce_log", "minecraft:oak_planks", "minecraft:spruce_planks"} <= all_names def test_deck_landing_and_every_room_floor_reachable(saved): """Walk from the mooring up the stairs, through the door, to every clear floor.""" nodes = walkable(saved) reached = reachable(nodes, (5, 4, 22)) assert nodes == reached, f"unreachable supported floors: {sorted(nodes - reached)}" assert set(DOORS) <= reached room = {p for p in nodes if 10 <= p[0] <= 22 and p[1] == 7 and 7 <= p[2] <= 15} assert len(room) > 85 assert room <= reached, room - reached assert all((5, y, z) in reached for z, y in ((21, 4), (20, 4), (19, 5), (18, 6), (17, 7))) for z, y in ((18, 5), (19, 4), (20, 3)): for x in range(4, 7): name, props = parse(saved.state(x, y, z)) assert name == "minecraft:spruce_stairs" and props["facing"] == "north" and props["half"] == "bottom" assert all((8, 7, z) in reached and (10, 7, z) in reached for z in (10, 11)) for (x, y, z), state in saved.states.items(): name, props = parse(state) if name in {"minecraft:barrel", "minecraft:chest", "minecraft:smoker", "minecraft:crafting_table", "minecraft:water_cauldron"} or name.endswith("_bed"): assert any((x + dx, 7, z + dz) in reached for dx, dz in HORIZONTAL), (x, y, z, state) def test_doors_and_bed_are_complete_matching_pairs(saved): """Check halves, orientation, hinge, support, adjacency, and bed-side access.""" door_cells = [] bed_cells = [] for (x, y, z), state in saved.states.items(): name, props = parse(state) if name.endswith("_door"): door_cells.append((x, y, z)) dy = 1 if props["half"] == "lower" else -1 other_name, other = parse(saved.state(x, y + dy, z)) assert name == other_name assert other["half"] != props["half"] assert all(other[k] == props[k] for k in ("facing", "hinge", "open", "powered")) assert props["open"] == "false" if props["half"] == "lower": assert saved.solid(x, y - 1, z) if name.endswith("_bed"): bed_cells.append((x, y, z)) dx, dz = FACING[props["facing"]] sign = 1 if props["part"] == "foot" else -1 other_name, other = parse(saved.state(x + dx * sign, y, z + dz * sign)) assert other_name == name and other["facing"] == props["facing"] and other["part"] != props["part"] assert saved.solid(x, y - 1, z) assert saved.name(x, y + 1, z) == AIR assert len(door_cells) == 4 and len(bed_cells) == 2 def test_all_room_and_attic_air_is_separated_from_sky(saved): """Flood exterior air and prove it cannot reach any habitable or attic pocket.""" # Small objects and non-solid furniture do not falsely stop the flood. def air_permeable(pos): name = saved.name(*pos) return name == AIR or name.endswith(("_carpet", "_fence", "_bed", "_stairs")) or name in {"minecraft:lantern", "minecraft:chain", "minecraft:flower_pot"} or name.startswith("minecraft:potted_") exterior = set() queue = deque() for pos in saved.states: x, y, z = pos if x in (0, saved.sx - 1) or y in (0, saved.sy - 1) or z in (0, saved.sz - 1): if air_permeable(pos): exterior.add(pos) queue.append(pos) while queue: x, y, z = queue.popleft() for dx, dy, dz in DIRECTIONS: p = (x + dx, y + dy, z + dz) if p in saved.states and p not in exterior and air_permeable(p): exterior.add(p) queue.append(p) checked = 0 for x in range(10, 23): for z in range(7, 16): roof_backing = 11 + (7 - abs(z - 11)) // 2 for y in range(7, roof_backing): p = (x, y, z) if air_permeable(p): assert p not in exterior, f"interior leaks at {p}" checked += 1 assert checked > 450 def test_windows_and_door_openings_stay_clear(saved): """Verify intact connected panes and no furnishings directly against the glass.""" for x, y, z in WINDOWS: name, props = parse(saved.state(x, y, z)) assert name == "minecraft:glass_pane" if z in (6, 16): assert props["east"] == props["west"] == "true" inner = (x, y, z + (1 if z == 6 else -1)) outer = (x, y, z + (-1 if z == 6 else 1)) else: assert props["north"] == props["south"] == "true" inner = (x + (1 if x == 9 else -1), y, z) outer = (x + (-1 if x == 9 else 1), y, z) assert saved.name(*inner) == AIR, (inner, saved.state(*inner)) assert saved.name(*outer) == AIR, (outer, saved.state(*outer)) for x, y, z in DOORS: assert saved.name(x - 1, y, z) == saved.name(x + 1, y, z) == AIR assert saved.name(x - 1, y + 1, z) == saved.name(x + 1, y + 1, z) == AIR def test_roof_overhang_and_lighting(saved): """Check the dark timber roof, sheltered entry, and nearby room lights.""" roof_names = {saved.name(x, y, z) for x in range(7, 25) for y in range(12, 17) for z in range(4, 19)} assert {"minecraft:dark_oak_planks", "minecraft:dark_oak_stairs", "minecraft:dark_oak_slab"} <= roof_names assert all(saved.name(6, 15, z) != AIR for z in (10, 11, 12)) lanterns = {p for p, s in saved.states.items() if parse(s)[0] == "minecraft:lantern"} assert lanterns == set(LIGHTS) | {(7, 10, 18), (24, 10, 18)} for pos in walkable(saved): x, y, z = pos if 10 <= x <= 22 and 7 <= z <= 15: assert min(max(abs(x - lx), abs(z - lz)) for lx, ly, lz in LIGHTS) <= 3 assert min(abs(y - ly) for lx, ly, lz in LIGHTS) <= 3 for x, y, z in lanterns: assert parse(saved.state(x, y, z))[1]["hanging"] == "true" assert saved.name(x, y + 1, z) == "minecraft:chain" assert saved.solid(x, y + 2, z) or saved.name(x, y + 2, z).endswith("_slab") def test_decorations_have_real_support(saved): """Check every small furnishing and plant, including wall and ceiling attachments.""" leaves = set() for (x, y, z), state in saved.states.items(): name, props = parse(state) below = saved.name(x, y - 1, z) if name.endswith("_leaves"): assert props["persistent"] == "true" leaves.add((x, y, z)) if name.endswith("_carpet") or name in {"minecraft:chest", "minecraft:barrel", "minecraft:crafting_table", "minecraft:smoker", "minecraft:water_cauldron"}: assert saved.solid(x, y - 1, z), (x, y, z, name) if name == "minecraft:flower_pot" or name.startswith("minecraft:potted_"): assert saved.solid(x, y - 1, z) or (below.endswith("_slab") and parse(saved.state(x, y - 1, z))[1]["type"] == "top") if name.endswith("_fence"): assert saved.solid(x, y - 1, z), (x, y, z) if name.endswith("_trapdoor"): assert any(saved.solid(x + dx, y, z + dz) for dx, dz in HORIZONTAL), (x, y, z) if name == "minecraft:sugar_cane": assert below in {"minecraft:sugar_cane", "minecraft:mud", "minecraft:dirt"} if below != "minecraft:sugar_cane": assert any(saved.name(x + dx, y - 1, z + dz) == "minecraft:water" for dx, dz in HORIZONTAL) if name == "minecraft:lily_pad": assert below == "minecraft:water" if name == "minecraft:seagrass": assert saved.solid(x, y - 1, z) leaf_roots = {p for p in leaves if any(saved.solid(p[0] + dx, p[1] + dy, p[2] + dz) for dx, dy, dz in DIRECTIONS)} assert leaf_roots connected = set(leaf_roots) queue = deque(leaf_roots) while queue: x, y, z = queue.popleft() for dx, dy, dz in DIRECTIONS: p = x + dx, y + dy, z + dz if p in leaves - connected: connected.add(p) queue.append(p) assert connected == leaves assert sum(parse(s)[0] == "minecraft:lily_pad" for s in saved.states.values()) >= 10 # Every chest has vertical space for its lid. for (x, y, z), state in saved.states.items(): if parse(state)[0] == "minecraft:chest": assert saved.name(x, y + 1, z) == AIR def test_saved_boat_and_stocked_containers(saved): """Inspect persisted NBT that block-only previews cannot show.""" boats = list(saved.region["Entities"]) assert len(boats) == 1 boat = boats[0] assert str(boat["id"]) == "minecraft:boat" and str(boat["Type"]) == "oak" assert tuple(float(n) for n in boat["Pos"]) == BOAT_POSITION assert len(boat["UUID"]) == 4 bx, by, bz = BOAT_POSITION assert abs(bx - 8.5) < 3 and abs(bz - 21.5) < 3 # The 1.375-block-wide hull does not intersect the landing or plants. for x in range(int(bx - 0.6875), int(bx + 0.6875) + 1): for z in range(int(bz - 0.6875), int(bz + 0.6875) + 1): assert saved.name(x, 2, z) == "minecraft:water" assert saved.name(x, 3, z) == AIR entities = list(saved.region["TileEntities"]) assert len(entities) == 8 contained = set() positions = set() for entity in entities: p = tuple(int(entity[k]) for k in ("x", "y", "z")) assert p not in positions positions.add(p) assert saved.name(*p) == str(entity["id"]) items = list(entity["Items"]) assert items assert len({int(i["Slot"]) for i in items}) == len(items) for item in items: assert str(item["id"]).startswith("minecraft:") assert int(item["count"]) > 0 assert "Count" not in item contained.add(str(item["id"])) assert {"minecraft:fishing_rod", "minecraft:cod", "minecraft:salmon", "minecraft:charcoal", "minecraft:bread"} <= contained def test_clean_directory_rerun_is_byte_identical(saved): """Run only the copied generator with no arguments and compare the full file.""" with tempfile.TemporaryDirectory(prefix="rerun_", dir=ROOT / "test") as temp: work = Path(temp) shutil.copy2(ROOT / "generate.py", work / "generate.py") result = subprocess.run([sys.executable, "generate.py"], cwd=work, text=True, capture_output=True, check=True) assert "verified 8 stocked block entities and 1 oak boat entity" in result.stdout assert (work / "output.litematic").read_bytes() == (ROOT / "output.litematic").read_bytes() build = Build().build() build.verify(work / "output.litematic")
stilted-manor — 9 个测试 / 286 行 / test_structure.py

test_export_contract_and_all_states 材质与状态
test_reproducible_roundtrip 确定性与导出
test_complete_doors_and_beds 可达性与流线
test_all_rooms_reachable_with_two_blocks_headroom 可达性与流线
test_envelope_sealed_including_nonfull_roof_blocks 封闭性与外壳
test_supported_decorations_and_ladders 可达性与流线
test_windows_and_doorways_clear 可达性与流线
test_real_overhang_and_braced_support 支撑与悬空
test_rooms_have_nearby_lighting 光照

# Validate the exported manor's geometry, circulation, state pairs, and reproducibility. from collections import deque import json from pathlib import Path import sys import subprocess import tempfile import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, load_schematic from mcio.nbt.nbt import File as NBTFile DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)} ROOMS = { 'entrance': (19, 3, 11), 'storage': (17, 3, 20), 'workshop': (21, 3, 18), 'great room': (17, 10, 18), 'hearth': (21, 10, 12), 'west bedroom': (11, 17, 15), 'east bedroom': (21, 17, 17), 'cupola lookout': (17, 28, 15), } def split_state(state): """Read a palette block name and its property mapping.""" name, _, rest = state.partition('[') return name.removeprefix('minecraft:'), dict(item.split('=') for item in rest.rstrip(']').split(',') if item) class Volume: """Expose the reloaded schematic in XYZ coordinates for spatial checks.""" def __init__(self, path): self.loaded = load_schematic(path) self.ids = self.loaded.read_flat(0, self.loaded.volume).reshape(self.loaded.size_yzx) self.states = [split_state(s) for s in self.loaded.palette] self.sy, self.sz, self.sx = self.ids.shape def inside(self, p): x, y, z = p return 0 <= x < self.sx and 0 <= y < self.sy and 0 <= z < self.sz def at(self, p): if not self.inside(p): return 'air', {} x, y, z = p return self.states[int(self.ids[y, z, x])] def cells(self): for y in range(self.sy): for z in range(self.sz): for x in range(self.sx): yield (x, y, z), self.at((x, y, z)) def walk_clear(self, p): name, _ = self.at(p) return name in ['air', 'ladder'] or name.endswith('_door') or name.endswith('_carpet') def full(self, p): name, props = self.at(p) if name == 'air' or name in ['lantern', 'chain', 'ladder', 'iron_bars', 'short_grass', 'fern', 'azalea', 'flowering_azalea', 'campfire', 'stonecutter', 'anvil', 'flower_pot', 'cornflower', 'poppy', 'dandelion', 'azure_bluet', 'oxeye_daisy']: return False if name.startswith('potted_') or name.endswith(('_carpet', '_bed', '_trapdoor', '_stairs', '_wall', '_fence', '_leaves', 'candle')): return False if name.endswith('_slab'): return props.get('type') == 'double' return True def supports(self, p): name, props = self.at(p) return self.full(p) or name.endswith('_stairs') or (name.endswith('_slab') and props.get('type') in ['top', 'double']) def stance(self, p): x, y, z = p return self.inside((x, y + 1, z)) and self.walk_clear(p) and self.walk_clear((x, y + 1, z)) and (self.supports((x, y - 1, z)) or self.at(p)[0] == 'ladder') def reachable(self, start): """Search two-block-tall standing states, using aligned stairs and ladders.""" assert self.stance(start), start seen = {start} queue = deque([start]) while queue: x, y, z = queue.popleft() current_ladder = self.at((x, y, z))[0] == 'ladder' neighbors = [] if current_ladder: neighbors += [(x, y - 1, z), (x, y + 1, z)] for face, (dx, dz) in DIRECTIONS.items(): for dy in [-1, 0, 1]: q = x + dx, y + dy, z + dz if dy: step = q if dy > 0 else (x, y, z) sn, sp = self.at((step[0], step[1] - 1, step[2])) expected = face if dy > 0 else next(f for f, d in DIRECTIONS.items() if d == (-dx, -dz)) if not current_ladder and not (sn.endswith('_stairs') and sp['facing'] == expected): continue # The higher destination must also clear the source head. if dy > 0 and not self.walk_clear((x, y + 2, z)): continue neighbors.append(q) for q in neighbors: if q not in seen and self.stance(q): seen.add(q) queue.append(q) return seen @pytest.fixture(scope='module') def v(): return Volume(ROOT / 'output.litematic') def test_export_contract_and_all_states(v): assert (v.sx, v.sy, v.sz) == (33, 39, 31) nbt = NBTFile.load(ROOT / 'output.litematic', gzipped=False) assert int(nbt['MinecraftDataVersion']) == 3955 assert len(nbt['Regions']) == 1 registry = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for name, props in v.states: assert name in registry, name choices = registry[name][0] for key, value in props.items(): assert key in choices and value in choices[key], (name, key, value) assert np.count_nonzero(v.ids) > 6500 def test_reproducible_roundtrip(): # Temporary outputs stay in test/ and are deleted on success or failure. with tempfile.TemporaryDirectory(dir=ROOT / 'test') as directory: a, b = Path(directory) / 'a.litematic', Path(directory) / 'output.litematic' first = Build().build() first.export(a) first.verify(a) subprocess.run([sys.executable, str(ROOT / 'generate.py')], cwd=directory, check=True, capture_output=True, timeout=30) assert a.read_bytes() == b.read_bytes() == (ROOT / 'output.litematic').read_bytes() def test_complete_doors_and_beds(v): doors, beds = 0, 0 for (x, y, z), (name, props) in v.cells(): if name.endswith('_door'): dy = 1 if props['half'] == 'lower' else -1 other_name, other = v.at((x, y + dy, z)) assert other_name == name assert other['half'] != props['half'] assert all(other[key] == props[key] for key in ['facing', 'hinge', 'open', 'powered']) if props['half'] == 'lower': assert v.supports((x, y - 1, z)) doors += 1 if name.endswith('_bed'): dx, dz = DIRECTIONS[props['facing']] sign = 1 if props['part'] == 'foot' else -1 other_name, other = v.at((x + sign * dx, y, z + sign * dz)) assert other_name == name and other['part'] != props['part'] assert other['facing'] == props['facing'] assert v.supports((x, y - 1, z)) beds += props['part'] == 'foot' assert doors == 4 and beds == 4 def test_all_rooms_reachable_with_two_blocks_headroom(v): reached = v.reachable((19, 2, 4)) for room, target in ROOMS.items(): assert target in reached, f'{room} is unreachable: {target}' # Every bed and useful work station has a reachable adjacent standing cell. for (x, y, z), (name, props) in v.cells(): if name in ['crafting_table', 'smithing_table', 'stonecutter', 'furnace', 'anvil'] or (name.endswith('_bed') and props['part'] == 'foot'): assert any((x + dx, y, z + dz) in reached for dx, dz in DIRECTIONS.values()), (name, x, y, z) def test_envelope_sealed_including_nonfull_roof_blocks(v): # Treat stairs, slabs, furnishings and plants as permeable. Full backing # masonry, glass and closed doors must still isolate every interior cell. outside = set() queue = deque() for p, state in v.cells(): x, y, z = p if x in [0, v.sx - 1] or y in [0, v.sy - 1] or z in [0, v.sz - 1]: if not v.full(p): outside.add(p) queue.append(p) while queue: x, y, z = queue.popleft() for dx, dy, dz in [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)]: q = x + dx, y + dy, z + dz if v.inside(q) and q not in outside and not v.full(q): outside.add(q) queue.append(q) interior = set() for lo, hi in [((9, 3, 10), (23, 8, 21)), ((6, 10, 7), (26, 15, 23)), ((16, 28, 14), (18, 31, 16))]: for x in range(lo[0], hi[0] + 1): for y in range(lo[1], hi[1] + 1): for z in range(lo[2], hi[2] + 1): interior.add((x, y, z)) for x in range(6, 27): for z in range(7, 24): roof_bottom = 16 + min(z - 4, 26 - z) for y in range(17, min(27, roof_bottom)): interior.add((x, y, z)) for center in [10, 22]: for x in range(center - 1, center + 2): for z in range(8, 12): for y in range(18, 24 - abs(x - center)): interior.add((x, y, z)) leaks = sorted(p for p in interior if not v.full(p) and p in outside) assert not leaks, f'{len(leaks)} interior cells open to outdoors, first: {leaks[:20]}' def test_supported_decorations_and_ladders(v): floor_items = {'flower_pot', 'short_grass', 'fern', 'azalea', 'flowering_azalea', 'cornflower', 'poppy', 'dandelion', 'azure_bluet', 'oxeye_daisy', 'anvil', 'stonecutter', 'campfire', 'chest', 'barrel'} for (x, y, z), (name, props) in v.cells(): below = v.at((x, y - 1, z))[0] if name in floor_items or name.startswith('potted_') or name.endswith(('_carpet', 'candle')): assert v.supports((x, y - 1, z)) or (name == 'flower_pot' and below.endswith('_wall')), (name, x, y, z, below) if name == 'ladder': dx, dz = DIRECTIONS[props['facing']] assert v.full((x - dx, y, z - dz)), ('ladder backing', x, y, z) if name == 'lantern': anchor = (x, y + 1, z) if props['hanging'] == 'true' else (x, y - 1, z) assert v.supports(anchor) or v.at(anchor)[0] in ['chain', 'lantern'], ('lantern', x, y, z, v.at(anchor)) if name == 'chain': assert v.full((x, y + 1, z)) or v.at((x, y + 1, z))[0] == 'chain' if name.endswith('_trapdoor'): dx, dz = DIRECTIONS[props['facing']] assert v.supports((x - dx, y, z - dz)), ('unattached shutter', x, y, z) def test_windows_and_doorways_clear(v): for z in [6, 24]: inside = z + (1 if z == 6 else -1) for lo, hi in [(8, 10), (14, 16), (21, 23)]: for x in range(lo, hi + 1): for y in range(11, 15): assert v.at((x, y, z))[0].endswith('glass'), (x, y, z) # Low furniture may sit below the window but must not fill it. assert not v.full((x, y, inside)), ('window blocked inside', x, y, inside) for x in [5, 27]: for lo, hi in [(8, 10), (16, 18), (21, 22)]: for z in range(lo, hi + 1): for y in range(11, 15): assert v.at((x, y, z))[0].endswith('glass'), (x, y, z) assert not v.full((x + (1 if x == 5 else -1), y, z)), ('side window blocked', x, y, z) for z in [12, 13, 17, 18]: for y in range(19, 22): assert v.at((x, y, z))[0].endswith('glass') assert not v.full((x + (1 if x == 5 else -1), y, z)), ('gable window blocked', x, y, z) for center in [10, 22]: for x in range(center - 1, center + 2): for y in range(19, 22): assert v.at((x, y, 7))[0] == 'glass' assert v.at((x, y, 8))[0] == 'air' for x in [19, 20]: for y in [3, 4]: assert v.walk_clear((x, y, 8)) and v.walk_clear((x, y, 10)) def test_real_overhang_and_braced_support(v): for x in [5, 27]: for z in [6, 15, 24]: assert all(v.at((x, y, z))[0].endswith('_log') for y in range(3, 9)) # Stone footprint recessed three blocks; supported upper deck projects out. assert v.full((5, 9, 12)) and v.full((27, 9, 12)) assert v.at((6, 4, 12))[0] == 'air' and v.at((26, 4, 12))[0] == 'air' assert v.at((8, 4, 12))[0] in ['stone_bricks', 'cobblestone', 'andesite'] braces = sum(name == 'spruce_stairs' and props['half'] == 'top' and y in [7, 8] for (x, y, z), (name, props) in v.cells()) assert braces >= 25 for center in [10, 22]: assert v.at((center, 20, 7))[0] == 'glass' assert v.at((17, 38, 15))[0] == 'iron_bars' assert all(v.at((x, 32, 12))[0] == 'brick_wall' for x in [23, 25]) def test_rooms_have_nearby_lighting(v): lights = [] for p, (name, props) in v.cells(): if name == 'lantern' or name in ['campfire', 'furnace', 'candle'] and props.get('lit') == 'true': lights.append(p) for room, p in ROOMS.items(): nearby = [q for q in lights if 0 <= q[1] - p[1] <= 4 and abs(q[0] - p[0]) + abs(q[2] - p[2]) <= 6] assert nearby, (room, p)
merchant-manor — 10 个测试 / 408 行 / test_manor.py

test_format_scale_and_vanilla_states 尺寸与比例
test_doors_and_beds_are_complete 可达性与流线
test_all_rooms_and_front_stairs_are_reachable 可达性与流线
test_closed_envelope 封闭性与外壳
test_windows_and_entry_clearance 可达性与流线
test_lighting_near_each_room_floor 光照
test_decoration_support_and_ladders 可达性与流线
test_stocked_containers_frames_and_lectern_nbt 家具与内饰
test_chimney_smoke_and_required_material_families 材质与状态
test_rerun_is_byte_reproducible_and_reload_exact 确定性与导出

# Validate the exported manor's structure, circulation, envelope, furnishings and deterministic NBT. from __future__ import annotations from collections import deque from pathlib import Path import json import shutil import sys import subprocess import tempfile import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG, NBTFile, load_schematic DIRECTIONS = {'east': (1, 0, 0), 'west': (-1, 0, 0), 'south': (0, 0, 1), 'north': (0, 0, -1)} NEIGHBORS = list(DIRECTIONS.values()) + [(0, 1, 0), (0, -1, 0)] ROOMS = { 'cooperage': (6, 2, 12, 20, 7, 27), 'market stockroom': (22, 2, 12, 30, 7, 27), 'hall and kitchen': (8, 9, 14, 28, 14, 25), 'west bedroom': (8, 16, 14, 14, 19, 18), 'east bedroom': (24, 16, 14, 28, 20, 24), 'map room': (16, 16, 18, 22, 21, 24), 'study': (16, 23, 18, 22, 27, 24), 'lookout': (16, 29, 18, 22, 32, 24), 'turret lower landing': (8, 16, 20, 10, 22, 22), 'turret lookout': (8, 24, 20, 10, 28, 22), } TARGETS = { 'cooperage': (15, 2, 19), 'market stockroom': (25, 2, 20), 'hall and kitchen': (18, 9, 17), 'west bedroom': (11, 16, 17), 'east bedroom': (25, 16, 19), 'map room': (19, 16, 22), 'study': (20, 23, 22), 'lookout': (20, 29, 22), 'turret lower landing': (9, 16, 21), 'turret lookout': (9, 24, 21), } # The authored plan is reflected once so the stall appears at the viewer's right. def front_position(p): """Map an architectural plan coordinate into the final reference orientation. Args: p (tuple): Authored XYZ coordinate. Returns: tuple: Physical schematic coordinate. """ return CONFIG['size_xyz'][0] - 1 - p[0], p[1], p[2] ROOMS = {name: (CONFIG['size_xyz'][0] - 1 - x1, y0, z0, CONFIG['size_xyz'][0] - 1 - x0, y1, z1) for name, (x0, y0, z0, x1, y1, z1) in ROOMS.items()} TARGETS = {name: front_position(p) for name, p in TARGETS.items()} def cells(bounds): """Iterate inclusive XYZ bounds. Args: bounds (tuple): Minimum and maximum coordinates. Yields: tuple: An XYZ coordinate. """ 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): yield x, y, z def parse(state): """Split a block state into its name and properties. Args: state (str): Namespaced state string. Returns: tuple: Short block name and property mapping. """ name, _, tail = state.partition('[') return name.removeprefix('minecraft:'), dict(part.split('=') for part in tail.rstrip(']').split(',') if '=' in part) class Volume: """Read all test data from the actual exported artifact.""" def __init__(self, path): """Load palette, volume and raw entity NBT. Args: path (Path): Schematic to inspect. """ loaded = load_schematic(path) self.ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) self.sy, self.sz, self.sx = loaded.size_yzx self.palette = [parse(state) for state in loaded.palette] self.nbt = NBTFile.load_regardless_of_gzipped(path) self.region = next(iter(self.nbt['Regions'].values())) def at(self, p): """Get a parsed block or outside sentinel. Args: p (tuple): XYZ coordinate. Returns: tuple: Block name and properties. """ x, y, z = p if 0 <= x < self.sx and 0 <= y < self.sy and 0 <= z < self.sz: return self.palette[int(self.ids[y, z, x])] return 'outside', {} def inside(self, p): """Check coordinate bounds. Args: p (tuple): XYZ coordinate. Returns: bool: Whether the coordinate exists. """ return self.at(p)[0] != 'outside' def walk_air(self, p): """Allow air, climbable ladders, carpets and operable doors. Args: p (tuple): XYZ coordinate. Returns: bool: Two-block player volume may pass through this block. """ n, _ = self.at(p) return n in ('air', 'ladder', 'vine', 'short_grass', 'fern', 'oxeye_daisy', 'dandelion') or n.endswith(('_carpet', '_door')) def supports(self, p): """Accept walkable upper surfaces while excluding fences and thin decor. Args: p (tuple): XYZ coordinate. Returns: bool: The block supports a standing player. """ n, props = self.at(p) return n not in ('air', 'outside', 'ladder', 'vine', 'lantern', 'iron_bars', 'flower_pot', 'short_grass', 'fern', 'oxeye_daisy', 'dandelion') and not n.endswith(('_fence', '_wall', '_door', '_carpet', '_candle')) def node(self, p): """Require support and at least two blocks of clear headroom. Args: p (tuple): Integer feet coordinate. Returns: bool: A conservative standing or climbing position. """ x, y, z = p return self.walk_air(p) and self.walk_air((x, y + 1, z)) and (self.supports((x, y - 1, z)) or self.at(p)[0] == 'ladder') def reachable(self, start): """Flood valid player positions using stairs and ladder climbs, without jumping. Args: start (tuple): Starting feet position. Returns: set: Every reachable standing position. """ assert self.node(start), f'Blocked start: {start}' seen, todo = {start}, deque([start]) while todo: x, y, z = todo.popleft() for direction, (dx, _, dz) in DIRECTIONS.items(): for dy in (0, -1, 1): q = (x + dx, y + dy, z + dz) if q in seen or not self.node(q): continue if dy == 1: stair, props = self.at((q[0], q[1] - 1, q[2])) if not stair.endswith('_stairs') or props.get('facing') != direction or not self.walk_air((x, y + 2, z)): continue if dy == -1: stair, props = self.at((x, y - 1, z)) if not stair.endswith('_stairs') or not self.walk_air((q[0], y + 1, q[2])): continue seen.add(q) todo.append(q) if self.at((x, y, z))[0] == 'ladder' or self.at((x, y - 1, z))[0] == 'ladder': for dy in (-1, 1): q = (x, y + dy, z) if q not in seen and self.node(q): seen.add(q) todo.append(q) return seen @pytest.fixture(scope='module') def volume(): """Read the deliverable once for all structural assertions.""" return Volume(ROOT / 'output.litematic') @pytest.fixture(scope='module') def generated(): """Rebuild the specification without writing files.""" return Build().build() @pytest.fixture(scope='module') def reached(volume): """Start at the main front door on the terrace.""" return volume.reachable(front_position((17, 9, 13))) def test_format_scale_and_vanilla_states(volume): """Require exactly one Java 1.21.1 region and legal states at the intended scale.""" assert len(volume.nbt['Regions']) == 1 assert int(volume.nbt['MinecraftDataVersion']) == 3955 assert (volume.sx, volume.sy, volume.sz) == (37, 43, 33) registry = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for name, properties in volume.palette: assert name in registry, name for key, value in properties.items(): assert key in registry[name][0] and value in registry[name][0][key], (name, key, value) assert (volume.ids != 0).sum() > 6500 def test_doors_and_beds_are_complete(volume): """Verify every half, orientation, support, and clear bed sleeping space.""" doors, beds = 0, 0 for p in cells((0, 0, 0, volume.sx - 1, volume.sy - 1, volume.sz - 1)): n, props = volume.at(p) x, y, z = p if n.endswith('_door'): dy = 1 if props['half'] == 'lower' else -1 other, op = volume.at((x, y + dy, z)) assert n == other and op['half'] != props['half'], ('door half', p) assert all(op[k] == props[k] for k in ('facing', 'hinge', 'open')), ('door state', p) if dy == 1: doors += 1 assert volume.supports((x, y - 1, z)), ('door support', p) if n.endswith('_bed'): dx, _, dz = DIRECTIONS[props['facing']] sign = 1 if props['part'] == 'foot' else -1 other, op = volume.at((x + dx * sign, y, z + dz * sign)) assert n == other and op['part'] != props['part'] and props['facing'] == op['facing'], ('bed pair', p) assert volume.supports((x, y - 1, z)), ('bed support', p) assert volume.walk_air((x, y + 1, z)) and volume.walk_air((x, y + 2, z)), ('bed headroom', p) beds += props['part'] == 'foot' assert doors >= 8 and beds == 3 def test_all_rooms_and_front_stairs_are_reachable(volume, reached): """Reach all named rooms and every broad exterior step with two-block headroom.""" missing = {name: p for name, p in TARGETS.items() if p not in reached} assert not missing, f'Unreachable rooms: {missing}' assert front_position((17, 1, 2)) in reached, 'Outside stair does not reach the street' for z in range(3, 11): for x in range(15, 20): assert front_position((x, z - 1, z)) in reached, ('front stair', x, z) for name, bounds in ROOMS.items(): x0, y0, z0, x1, _, z1 = bounds level_positions = [p for p in cells((x0, y0, z0, x1, y0, z1)) if volume.node(p)] assert level_positions and all(p in reached for p in level_positions), (name, [p for p in level_positions if p not in reached]) def test_closed_envelope(volume): """Flood exterior air through thin decor and reject any interior leak to the sky.""" def permeable(p): n, _ = volume.at(p) return n in ('air', 'ladder', 'vine', 'lantern', 'iron_bars', 'flower_pot', 'short_grass', 'fern', 'oxeye_daisy', 'dandelion', 'chain') or n.endswith(('_carpet', '_candle', '_fence', '_leaves')) or n.startswith('potted_') outside, todo = set(), deque() for p in cells((0, 0, 0, volume.sx - 1, volume.sy - 1, volume.sz - 1)): x, y, z = p if x in (0, volume.sx - 1) or y in (0, volume.sy - 1) or z in (0, volume.sz - 1): if permeable(p): outside.add(p) todo.append(p) while todo: x, y, z = todo.popleft() for dx, dy, dz in NEIGHBORS: q = (x + dx, y + dy, z + dz) if q not in outside and volume.inside(q) and permeable(q): outside.add(q) todo.append(q) leaks = {name: [p for p in cells(bounds) if p in outside and volume.at(p)[0] == 'air'] for name, bounds in ROOMS.items()} leaks = {name: points[:8] for name, points in leaks.items() if points} assert not leaks, f'Interior connected to exterior air: {leaks}' def test_windows_and_entry_clearance(volume, generated): """Keep glass intact and exterior views clear of roofs, chimneys, and ivy.""" for x, y, z, dx, dz in generated.windows: assert volume.at((x, y, z))[0].endswith('_glass'), ('overwritten window', x, y, z, volume.at((x, y, z))) assert volume.at((x + dx, y, z + dz))[0] in ('air', 'vine'), ('blocked window', x, y, z, volume.at((x + dx, y, z + dz))) for p in [(17, 9, 12), (18, 9, 12), (17, 9, 14), (18, 9, 14), (10, 2, 10), (11, 2, 10), (26, 2, 10), (27, 2, 10)]: p = front_position(p) assert volume.walk_air(p) and volume.walk_air((p[0], p[1] + 1, p[2])), ('entry obstruction', p) def test_lighting_near_each_room_floor(volume): """Require a real, lit source close to every occupied floor area.""" for name, bounds in ROOMS.items(): x0, floor, z0, x1, y1, z1 = bounds lights = [] for p in cells(bounds): n, props = volume.at(p) if n == 'lantern' or (n.endswith('_candle') or n == 'campfire') and props.get('lit') == 'true': lights.append(p) assert lights, f'No light inside {name}' for p in cells((x0, floor, z0, x1, floor, z1)): if volume.node(p): assert min(sum(abs(a - b) for a, b in zip(p, light)) for light in lights) <= 10, ('dark floor area', name, p) def test_decoration_support_and_ladders(volume): """Check actual attachment surfaces for all fragile decorations and ladder rungs.""" for p in cells((0, 1, 0, volume.sx - 1, volume.sy - 2, volume.sz - 1)): n, props = volume.at(p) x, y, z = p if n == 'ladder': dx, _, dz = DIRECTIONS[props['facing']] assert volume.supports((x - dx, y, z - dz)), ('ladder support', p) if n == 'lantern' or n.endswith(('_candle', '_carpet')) or n.startswith('potted_') or n == 'flower_pot': assert volume.supports((x, y - 1, z)), ('floor decor support', n, p, volume.at((x, y - 1, z))) if n == 'vine': supports = [] for face, (dx, dy, dz) in DIRECTIONS.items(): if props.get(face) == 'true': supports.append(volume.supports((x + dx, y + dy, z + dz))) assert any(supports), ('vine support', p) if n in ('short_grass', 'fern', 'oxeye_daisy', 'dandelion'): assert volume.at((x, y - 1, z))[0] in ('grass_block', 'dirt', 'coarse_dirt', 'rooted_dirt'), ('plant soil', p) # Every solid or decorative block belongs to the ground-connected structure. seen = {(1, 0, 1)} todo = deque(seen) while todo: x, y, z = todo.popleft() for dx, dy, dz in NEIGHBORS: q = (x + dx, y + dy, z + dz) if q not in seen and volume.at(q)[0] not in ('air', 'outside'): seen.add(q) todo.append(q) floating = [p for p in cells((0, 0, 0, volume.sx - 1, volume.sy - 1, volume.sz - 1)) if volume.at(p)[0] != 'air' and p not in seen] assert not floating, ('unattached blocks', floating[:20]) def test_stocked_containers_frames_and_lectern_nbt(volume): """Verify renderer-invisible furnishings survive export and have solid attachments.""" counts = {'chest': 0, 'barrel': 0, 'lectern': 0} for tile in volume.region['TileEntities']: p = tuple(int(tile[k]) for k in ('x', 'y', 'z')) n = str(tile['id']).removeprefix('minecraft:') assert volume.at(p)[0] == n, ('overwritten block entity', p, n) counts[n] += 1 if n in ('chest', 'barrel'): assert tile['Items'] for stack in tile['Items']: assert 'count' in stack and int(stack['count']) > 0 and str(stack['id']).startswith('minecraft:') if n == 'chest': assert volume.walk_air((p[0], p[1] + 1, p[2])), ('chest lid blocked', p) else: assert tile['Book']['components']['minecraft:written_book_content']['pages'] assert counts['chest'] >= 7 and counts['barrel'] >= 15 and counts['lectern'] == 1 frames = volume.region['Entities'] assert len(frames) >= 8 for frame in frames: assert str(frame['id']) == 'minecraft:item_frame' and int(frame['Item']['count']) == 1 direction = {2: 'north', 3: 'south', 4: 'west', 5: 'east'}[int(frame['Facing'])] dx, _, dz = DIRECTIONS[direction] x, y, z = (int(frame[k]) for k in ('TileX', 'TileY', 'TileZ')) assert volume.supports((x - dx, y, z - dz)), ('frame support', x, y, z) assert volume.at((x, y, z))[0] in ('air', 'vine'), ('frame space', x, y, z) def test_chimney_smoke_and_required_material_families(volume): """Require a real signal fire and the reference's principal materials.""" assert volume.at(front_position((27, 33, 23))) == ('campfire', {'facing': 'north', 'lit': 'true', 'signal_fire': 'true'}) assert volume.at(front_position((27, 32, 23)))[0] == 'hay_block' assert all(volume.at(front_position((27, y, 23)))[0] == 'air' for y in range(34, volume.sy)) names = {name for name, _ in volume.palette} assert {'cobblestone', 'stone_bricks', 'andesite', 'polished_andesite', 'cobbled_deepslate', 'deepslate_bricks', 'deepslate_tiles', 'stripped_spruce_log', 'dark_oak_log', 'smooth_sandstone', 'cut_sandstone', 'red_wool', 'white_wool', 'bricks', 'vine', 'bookshelf'} <= names def test_rerun_is_byte_reproducible_and_reload_exact(generated): """Rebuild twice in a clean temporary directory and compare complete artifact bytes.""" temporary = Path(tempfile.mkdtemp(prefix='reproduce_', dir=ROOT / 'test')) try: first = generated.export(temporary / 'first.litematic') generated.verify(first) subprocess.run([sys.executable, str(ROOT / 'generate.py')], cwd=temporary, check=True, capture_output=True, text=True) second = temporary / 'output.litematic' assert first.read_bytes() == second.read_bytes() == (ROOT / 'output.litematic').read_bytes() nbt = NBTFile.load_regardless_of_gzipped(first) assert int(nbt['Metadata']['TimeCreated']) == int(nbt['Metadata']['TimeModified']) == 0 finally: shutil.rmtree(temporary)
mediterranean-villa — 11 个测试 / 419 行 / test_villa.py

test_version_region_bounds_and_vanilla_states 尺寸与比例
test_doors_complete_supported_and_clear 可达性与流线
test_beds_complete_and_supported 支撑与悬空
test_all_rooms_and_terraces_reachable_with_headroom 可达性与流线
test_envelope_separates_all_interior_air_from_sky 封闭性与外壳
test_all_decorations_have_support 支撑与悬空
test_foliage_is_connected_to_structure_or_ground 支撑与悬空
test_windows_clear_and_roofs_separate 封闭性与外壳
test_room_furnishings_lights_and_serialized_contents 光照
test_pool_and_open_pergolas 其它
test_rerun_is_byte_reproducible_and_matches_deliverable 确定性与导出

# Validate the delivered Minecraft volume, navigation, attached decorations and serialized entities. from collections import deque from pathlib import Path import sys import tempfile import json import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from generate import Build, CONFIG, DIRECTIONS from mcio.schematic import load_schematic from mcio.nbt.nbt import File as NBTFile RULES = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) ROOMS = { 'living': ((8, 5, 5, 16, 9, 15), (13, 5, 12)), 'first_hall': ((18, 5, 5, 23, 9, 15), (22, 5, 12)), 'enchanting': ((25, 5, 9, 30, 9, 14), (27, 5, 11)), 'kitchen': ((8, 11, 5, 13, 15, 15), (12, 11, 11)), 'brewing': ((15, 11, 5, 18, 15, 9), (17, 11, 8)), 'bedroom': ((20, 11, 5, 23, 15, 9), (23, 11, 9)), 'second_hall': ((14, 11, 11, 23, 15, 15), (21, 11, 13)), } def decode(state): """Parse a blockstate and apply the bundled Java defaults. Args: state: Serialized blockstate. Returns: tuple: Short block name and its complete properties. """ name = state.split('[')[0].removeprefix('minecraft:') props = dict(RULES[name][1]) if '[' in state: props.update(part.split('=') for part in state.split('[')[1].rstrip(']').split(',')) return name, props class World: """Read-only view of the actual delivered schematic.""" def __init__(self): """Load voxel data and original entity NBT from the deliverable.""" self.loaded = load_schematic(ROOT / 'output.litematic') self.ids = self.loaded.read_flat(0, self.loaded.volume).reshape(self.loaded.size_yzx) self.sy, self.sz, self.sx = self.loaded.size_yzx self.palette = [decode(s) for s in self.loaded.palette] self.nbt = NBTFile.load_regardless_of_gzipped(ROOT / 'output.litematic') self.region = next(iter(self.nbt['Regions'].values())) def at(self, x, y, z): """Read a block, using an impassable boundary outside the schematic. Args: x, y, z: Coordinates. Returns: tuple: Block name and properties. """ if not (0 <= x < self.sx and 0 <= y < self.sy and 0 <= z < self.sz): return 'boundary', {} return self.palette[int(self.ids[y, z, x])] def cells(self): """Iterate the complete volume. Yields: tuple: XYZ coordinate, block name and properties. """ for y in range(self.sy): for z in range(self.sz): for x in range(self.sx): name, props = self.at(x, y, z) yield (x, y, z), name, props @pytest.fixture(scope='module') def world(): """Load the delivered file once for structural tests. Returns: World: Reloaded block and NBT data. """ return World() def body_clear(name, props, doors=True): """Check two-block navigation clearance, allowing players to open wooden doors. Args: name: Block identifier. props: State properties. doors: Whether closed wooden doors can be opened. Returns: bool: The voxel is traversable by the player's body. """ return (name in {'air', 'cave_air', 'void_air', 'vine'} or name.endswith('_carpet') or (doors and name.endswith('_door') and name != 'iron_door')) def full_support(name, props): """Identify blocks with a complete upward supporting face. Args: name: Block identifier. props: State properties. Returns: bool: The block can support ordinary floor decorations. """ if name.endswith('_slab'): return props['type'] in {'top', 'double'} if name.endswith('_stairs'): return props['half'] == 'top' if name.endswith('_trapdoor'): return props['open'] == 'false' and props['half'] == 'top' return not (body_clear(name, props) or name in {'boundary', 'water', 'lava', 'chain', 'lantern', 'brewing_stand', 'lectern', 'enchanting_table', 'cake', 'allium', 'azure_bluet', 'short_grass', 'cocoa'} or name.startswith('potted_') or name.endswith(('_fence', '_wall', '_bed', '_candle', '_trapdoor'))) def test_version_region_bounds_and_vanilla_states(world): """Check Java version, exact region dimensions and every property against shipped assets.""" assert int(world.nbt['MinecraftDataVersion']) == 3955 assert len(world.nbt['Regions']) == 1 assert (world.sx, world.sy, world.sz) == (35, 27, 31) assert int(world.nbt['Metadata']['TimeCreated']) == 0 assert int(world.nbt['Metadata']['TimeModified']) == 0 assert int(world.nbt['Metadata']['TotalBlocks']) > 6000 for state in world.loaded.palette: name = state.split('[')[0].removeprefix('minecraft:') assert name in RULES, state if '[' in state: for key, value in (part.split('=') for part in state.split('[')[1].rstrip(']').split(',')): assert key in RULES[name][0] and value in RULES[name][0][key], state def test_doors_complete_supported_and_clear(world): """Check paired door states, threshold support and usable approaches on both sides.""" count = 0 for (x, y, z), name, props in world.cells(): if not name.endswith('_door'): continue dy = 1 if props['half'] == 'lower' else -1 other_name, other = world.at(x, y + dy, z) assert other_name == name, (x, y, z) assert other['half'] != props['half'] for key in ('facing', 'hinge', 'open', 'powered'): assert other[key] == props[key], (x, y, z, key) if dy == 1: count += 1 assert full_support(*world.at(x, y - 1, z)), (x, y, z) dx, dz = DIRECTIONS[props['facing']] for sign in (-1, 1): for h in (0, 1): assert body_clear(*world.at(x + sign * dx, y + h, z + sign * dz)), (x, y, z, sign, h) assert count == 9 def test_beds_complete_and_supported(world): """Check each head/foot pair, facing, support and clearance over the mattress.""" count = 0 for (x, y, z), name, props in world.cells(): if not name.endswith('_bed'): continue count += 1 dx, dz = DIRECTIONS[props['facing']] sign = 1 if props['part'] == 'foot' else -1 other_name, other = world.at(x + sign * dx, y, z + sign * dz) assert other_name == name assert other['facing'] == props['facing'] and other['part'] != props['part'] assert full_support(*world.at(x, y - 1, z)) assert body_clear(*world.at(x, y + 1, z)) assert count == 4 def walk_nodes(world): """Find standable cells with two full voxels of body clearance. Args: world: Reloaded schematic. Returns: dict: Walk nodes mapped to their support states. """ nodes = {} for (x, y, z), name, props in world.cells(): if y < 2 or y > 15 or not body_clear(name, props) or not body_clear(*world.at(x, y + 1, z)): continue below, state = world.at(x, y - 1, z) if full_support(below, state) or (below.endswith('_stairs') and state['half'] == 'bottom'): nodes[(x, y, z)] = (below, state) return nodes def reachable_nodes(world): """Traverse level floors and oriented stair flights without jumping or climbing furniture. Args: world: Reloaded schematic. Returns: set: Player foot positions reachable from the garden stair approach. """ nodes = walk_nodes(world) start = (18, 2, 30) assert start in nodes reached = {start} queue = deque([start]) while queue: x, y, z = queue.popleft() for direction, (dx, dz) in DIRECTIONS.items(): for dy in (-1, 0, 1): target = (x + dx, y + dy, z + dz) if target not in nodes or target in reached: continue if dy: higher = target if dy > 0 else (x, y, z) name, props = nodes[higher] if not name.endswith('_stairs'): continue expected = (dx, dz) if dy > 0 else (-dx, -dz) if DIRECTIONS[props['facing']] != expected: continue lower = (x, y, z) if dy > 0 else target if not body_clear(*world.at(lower[0], lower[1] + 2, lower[2])): continue reached.add(target) queue.append(target) return reached def test_all_rooms_and_terraces_reachable_with_headroom(world): """Trace a walking route from the garden through the front door into every named room.""" reachable = reachable_nodes(world) for name, (_, point) in ROOMS.items(): assert point in reachable, (name, point) for point in ((21, 5, 16), (22, 5, 16), (14, 11, 18), (26, 5, 20), (21, 5, 24)): assert point in reachable, point for z in range(9, 15): for x in (15, 16): assert (x, z - 3, z) in reachable, ('stair', x, z) def test_envelope_separates_all_interior_air_from_sky(world): """Flood exterior air and ensure no room air communicates with the sky through the closed shell.""" def open_cell(x, y, z): name, props = world.at(x, y, z) return body_clear(name, props, doors=False) or name in {'lantern', 'chain', 'brewing_stand'} or name.startswith('potted_') or name.endswith('_candle') outside = set() queue = deque() for (x, y, z), _, _ in world.cells(): if (x in (0, world.sx - 1) or y in (0, world.sy - 1) or z in (0, world.sz - 1)) and open_cell(x, y, z): queue.append((x, y, z)) outside.add((x, y, z)) while queue: x, y, z = queue.popleft() for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)): point = (x + dx, y + dy, z + dz) if point not in outside and open_cell(*point): outside.add(point) queue.append(point) for name, (bounds, _) in ROOMS.items(): 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): assert (x, y, z) not in outside, (name, 'leak', x, y, z) def test_all_decorations_have_support(world): """Check floor decorations, hanging lights, shutters, plants and entity attachment faces.""" for (x, y, z), name, props in world.cells(): below = world.at(x, y - 1, z) if name.startswith('potted_') or name.endswith(('_carpet', '_candle', '_bed')) or name in {'brewing_stand', 'lectern', 'cake', 'chest', 'enchanting_table', 'barrel', 'water_cauldron', 'smoker'}: assert full_support(*below), (name, x, y, z, 'below', below) if name in {'allium', 'azure_bluet', 'short_grass'}: assert below[0] in {'grass_block', 'moss_block', 'rooted_dirt', 'dirt', 'coarse_dirt'} if name == 'lantern': support = world.at(x, y + 1, z) if props['hanging'] == 'true' else below assert full_support(*support) or support[0] == 'chain' or support[0].endswith(('_wall', '_fence')), (name, x, y, z, support) if name == 'chain': support = world.at(x, y + 1, z) assert support[0] != 'air', ('chain', x, y, z) if name.endswith('_trapdoor') and props['open'] == 'true': dx, dz = DIRECTIONS[props['facing']] assert full_support(*world.at(x - dx, y, z - dz)), (name, x, y, z) if name == 'vine': supported = False for face, (dx, dz) in DIRECTIONS.items(): if props[face] == 'true' and full_support(*world.at(x + dx, y, z + dz)): supported = True if props['up'] == 'true' and full_support(*world.at(x, y + 1, z)): supported = True if world.at(x, y + 1, z)[0] == 'vine': supported = True assert supported, ('vine', x, y, z) if name == 'cocoa': dx, dz = DIRECTIONS[props['facing']] assert world.at(x + dx, y, z + dz)[0] in {'jungle_log', 'stripped_jungle_log'} facing_vectors = {2: (0, -1), 3: (0, 1), 4: (-1, 0), 5: (1, 0)} assert len(world.region['Entities']) == 7 for entity in world.region['Entities']: assert str(entity['id']) == 'minecraft:item_frame' x, y, z = (int(entity[key]) for key in ('TileX', 'TileY', 'TileZ')) dx, dz = facing_vectors[int(entity['Facing'])] assert full_support(*world.at(x - dx, y, z - dz)), ('frame', x, y, z) assert body_clear(*world.at(x, y, z)), ('frame obstructed', x, y, z) assert int(entity['Item']['count']) == 1 def test_foliage_is_connected_to_structure_or_ground(world): """Reject isolated leaf clusters and unsupported floating decorative geometry.""" solids = {point for point, name, _ in world.cells() if name not in {'air', 'water', 'vine', 'chain', 'lantern', 'cocoa'} and not name.startswith('potted_') and not name.endswith('_carpet')} reached = {point for point in solids if point[1] == 0} queue = deque(reached) while queue: x, y, z = queue.popleft() for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)): p = (x + dx, y + dy, z + dz) if p in solids and p not in reached: reached.add(p) queue.append(p) assert not (solids - reached), sorted(solids - reached) def test_windows_clear_and_roofs_separate(world): """Check every planned glass opening and protect front thresholds from roof or furniture intrusion.""" windows = [(10, 6, 16, 'south', 2), (10, 12, 16, 'south', 2), (20, 12, 16, 'south', 2), (20, 6, 4, 'north', 2), (16, 12, 4, 'north', 2), (21, 12, 4, 'north', 1), (24, 12, 7, 'east', 2), (27, 6, 15, 'south', 2), (31, 6, 11, 'east', 2), (27, 6, 8, 'north', 2)] for y in (6, 12): windows += [(10, y, 4, 'north', 2), (7, y, 7, 'west', 2), (7, y, 12, 'west', 2)] for x, y, z, face, width in windows: dx, dz = (1, 0) if face in {'north', 'south'} else (0, 1) nx, nz = DIRECTIONS[face] for i in range(width): for h in range(3): p = (x + i * dx, y + h, z + i * dz) assert world.at(*p)[0] == 'light_blue_stained_glass', ('glazing', p) outside = world.at(p[0] + nx, p[1], p[2] + nz)[0] assert outside == 'air', ('blocked view', p, outside) assert world.at(9, 24, 7)[0] == 'quartz_pillar' assert world.at(9, 25, 7)[0] == 'brick_slab' def test_room_furnishings_lights_and_serialized_contents(world): """Require named room furniture, nearby light sources and valid stocked NBT containers.""" expected = { 'living': {'white_candle', 'potted_fern', 'chest', 'brown_carpet', 'birch_slab'}, 'first_hall': {'light_blue_carpet', 'barrel', 'chest'}, 'enchanting': {'enchanting_table', 'bookshelf', 'lectern', 'chest', 'purple_carpet'}, 'kitchen': {'smoker', 'water_cauldron', 'cake', 'barrel'}, 'brewing': {'brewing_stand', 'water_cauldron', 'chest', 'barrel'}, 'bedroom': {'cyan_bed', 'barrel', 'spruce_trapdoor', 'cyan_carpet'}, 'second_hall': {'cyan_carpet', 'barrel', 'potted_blue_orchid'}, } lights = [(p, name) for p, name, props in world.cells() if name in {'lantern', 'sea_lantern'} or (name.endswith('_candle') and props['lit'] == 'true')] for room, (bounds, target) in ROOMS.items(): x0, y0, z0, x1, y1, z1 = bounds content = {world.at(x, y, z)[0] for x in range(x0, x1 + 1) for y in range(y0, y1 + 1) for z in range(z0, z1 + 1)} assert expected[room] <= content, (room, expected[room] - content) assert any(x0 <= x <= x1 and z0 <= z <= z1 and y0 <= y <= y0 + 3 for (x, y, z), _ in lights), room block_entities = world.region['TileEntities'] assert len(block_entities) >= 15 positions = set() for entity in block_entities: pos = tuple(int(entity[key]) for key in ('x', 'y', 'z')) assert pos not in positions positions.add(pos) name = str(entity['id']).removeprefix('minecraft:') assert world.at(*pos)[0] == name if name == 'lectern': assert str(entity['Book']['id']) == 'minecraft:written_book' assert world.at(*pos)[1]['has_book'] == 'true' else: assert entity['Items'] assert all(int(item['count']) > 0 and str(item['id']).startswith('minecraft:') for item in entity['Items']) # At least 15 power-contributing shelves with clear intermediate air. shelves = 0 for (x, y, z), name, _ in world.cells(): if name == 'bookshelf' and y in (5, 6) and max(abs(x - 28), abs(z - 11)) == 2: gap = (28 + (x - 28) // 2, y, 11 + (z - 11) // 2) if world.at(*gap)[0] == 'air': shelves += 1 assert shelves >= 15 def test_pool_and_open_pergolas(world): """Require a contained square pool at terrace height and airy timber pergola fields.""" for x in range(23, 30): for z in range(22, 29): assert world.at(x, 4, z)[0] == 'water' assert full_support(*world.at(x, 2, z)) for x in range(22, 31): for z in range(21, 30): if x in (22, 30) or z in (21, 29): assert full_support(*world.at(x, 4, z)) assert full_support(*world.at(x, 3, z)) for x0, x1, z0, z1, height in ((7, 16, 17, 21, 15), (25, 32, 17, 20, 10)): cells = [world.at(x, height, z)[0] for x in range(x0 + 1, x1) for z in range(z0 + 1, z1)] assert cells.count('air') / len(cells) >= .45 assert any(name.endswith('_slab') for name in cells) for x in (27, 30): assert world.at(x, 5, 18)[0] == 'cyan_wool' def test_rerun_is_byte_reproducible_and_matches_deliverable(): """Rebuild twice in an automatically cleaned test directory and compare the exported bytes.""" with tempfile.TemporaryDirectory(dir=ROOT / 'test') as directory: outputs = [Path(directory) / f'villa_{index}.litematic' for index in range(2)] for output in outputs: build = Build().build() build.export(output) build.verify(output) assert outputs[0].read_bytes() == outputs[1].read_bytes() == (ROOT / 'output.litematic').read_bytes()

材质与调色板

从产物本身数出来的方块名分布(不含空气)。八个建筑用到 203 种不同方块。

build方块名数用量前六只有它用的方块(前六)
root-house 52 dark_oak_log 727
stripped_dark_oak_log 653
dirt 332
stone 330
grass_block 310
cobblestone 270
brown_stained_glass, loom, stone_pressure_plate
sandstone-cottage 72 stone 1,955
spruce_planks 658
smooth_sandstone 597
dirt 343
grass_block 339
smooth_sandstone_stairs 296
tuff, birch_leaves, sand, polished_granite, polished_granite_slab, birch_log
stone-cottage 44 grass_block 220
stripped_oak_log 154
andesite 81
cobblestone 58
andesite_stairs 47
spruce_planks 41
stone_stairs, andesite_slab, brown_terracotta, sunflower, potted_dandelion
red-roof-farmstead 73 dirt 1,600
grass_block 1,348
stone 875
cobblestone 517
spruce_planks 491
andesite 487
mangrove_planks, mangrove_stairs, red_nether_bricks, red_nether_brick_stairs, farmland, wheat
stilt-water-cabin 40 water 1,074
mud 379
dirt 272
oak_planks 235
spruce_planks 232
oak_log 231
mud, seagrass, sugar_cane, lily_pad, potted_spruce_sapling
stilted-manor 64 dirt 1,049
grass_block 880
spruce_planks 806
stone_bricks 796
stripped_spruce_log 743
stone_brick_stairs 442
spruce_leaves, mossy_stone_brick_stairs, flowering_azalea, anvil, cobblestone_wall
merchant-manor 78 spruce_planks 1,271
stone_bricks 798
grass_block 762
smooth_sandstone 455
stripped_spruce_log 408
cobblestone 335
cobbled_deepslate_stairs, cobbled_deepslate, deepslate_bricks, deepslate_tiles, polished_andesite, deepslate_tile_stairs
mediterranean-villa 87 dirt 1,069
diorite 995
grass_block 830
smooth_quartz 567
oak_planks 375
white_concrete 347
smooth_quartz, white_concrete, calcite, orange_terracotta, light_blue_terracotta, jungle_leaves

八个 build 都用到的方块(4 种):barrel, grass_block, lantern, spruce_slab。 这批是地基和填充用的通用件,说明八个任务在『骨架材料』上是趋同的,差异全在饰面层。

Fig 14. 各 build 用量前六的方块占比(占该 build 非空气方块的比例)。

八份代码

八份 generate.py 全部从同一个 template_generator.py 出发。 下表列出在两个以上 build 里出现过的函数名:模板自带的 10 个全员保留, 另外 24 个是模型自己发明并且撞名的。

函数名root-housesandstone-cottagestone-cottagered-roof-farmsteadstilt-water-cabinstilted-manormerchant-manormediterranean-villa出现
main8
shell8
box8
inside8
build8
get8
put8
material8
texture8
__init__8
export8
verify8
terrain5
roof5
door4
garden3
structure3
p3
mix3
fill3
orient_front2
furnish2
lantern2
slab2
interiors2
furnishings2
connect_fences2
greenery2
inventory2
timber2
planter2
bed2
window2
frame2

模板自带:shell, box, inside, build, get, put, material, texture, export, verify
自发撞名:main, __init__, terrain, roof, door, garden, structure, p, mix, fill, orient_front, furnish, lantern, slab, interiors, furnishings, connect_fences, greenery, inventory, timber, planter, bed, window, frame

完整源码

root-house/generate.py — 632 行
# Generate and verify a furnished Java 1.21.1 Root House with deterministic block and entity data. from __future__ import annotations import argparse import gzip import io import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Compound, Int, Byte, String, Double, Float, IntArray, List as NBTList from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (25, 23, 22), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Root House", "author": "generator", "description": "A hearth and sleeping loft sheltered beneath ancient dark oak roots. Front faces north (-Z).", } AIR = 'minecraft:air' ROCK = ['minecraft:stone'] * 4 + ['minecraft:cobblestone'] * 3 + ['minecraft:mossy_cobblestone'] * 3 + ['minecraft:andesite'] HILL_TOP = ['minecraft:moss_block'] * 5 + ['minecraft:mossy_cobblestone'] * 3 + ['minecraft:stone', 'minecraft:rooted_dirt'] GROUND_BASE = ['minecraft:dirt'] * 5 + ['minecraft:rooted_dirt'] * 2 + ['minecraft:stone'] GROUND_TOP = ['minecraft:grass_block'] * 7 + ['minecraft:moss_block'] * 2 + ['minecraft:coarse_dirt'] PATH = ['minecraft:andesite'] * 3 + ['minecraft:cobblestone'] * 3 + ['minecraft:stone', 'minecraft:mossy_cobblestone'] FLOOR = ['minecraft:spruce_planks'] * 8 + ['minecraft:dark_oak_planks'] CARDINAL = [(1, 0), (-1, 0), (0, 1), (0, -1)] MAIN_ROWS = {9: (7, 18), 10: (6, 18), 11: (6, 18), 12: (6, 18), 13: (6, 18), 14: (6, 18), 15: (6, 18), 16: (6, 18), 17: (7, 17), 18: (9, 16)} LOFT_ROWS = {10: (10, 15), 11: (8, 17), 12: (8, 17), 13: (8, 17), 14: (8, 17), 15: (8, 18), 16: (8, 18), 17: (9, 16)} Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} self.block_entities: list[Compound] = [] self.entities: list[Compound] = [] def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) return self.canvas.material(name, **dict(sorted(properties.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def build(self) -> "Build": """Build the rooted hillside, two furnished floors, and planted forecourt. Returns: Build: Completed build ready for export. """ self.terrain() self.roots() self.rooms() self.front() self.furnish() self.garden() self.orient_front() return self def terrain(self) -> None: """Raise an irregular stone hill above a rounded woodland ground patch.""" for x in range(self.size_x): for z in range(self.size_z): if ((x - 12) / 13.0)**2 + ((z - 11) / 12.2)**2 > 1.25: continue self.put(x, 0, z, self.rng.choice(GROUND_BASE)) self.put(x, 1, z, self.rng.choice(GROUND_TOP)) if z < 7: continue radial = ((x - 12) / 11.0)**2 + ((z - 13.5) / 9.0)**2 if radial >= 1: continue height = 2 + int(12.5 * (1 - radial)**0.58) for y in range(2, height + 1): palette = ROCK if y < height else HILL_TOP self.put(x, y, z, self.rng.choice(palette)) # An uneven flagstone walk disappears beneath the porch. for z in range(0, 9): for x in range(10, 14): if z < 3 and (x + z) % 5 == 0: continue self.put(x, 1, z, self.rng.choice(PATH)) self.texture((9, 2, 4, 15, 2, 8), PATH) for x in range(10, 14): self.put(x, 2, 3, 'minecraft:stone_brick_stairs', facing='south', half='bottom', shape='straight', waterlogged='false') def root(self, points: list[Position], radius: int = 1, front: bool = False) -> None: """Sweep overlapping squared timbers along an irregular root centerline. Args: points (list[Position]): XYZ nodes of the connected root. radius (int): Half width of the thick root. front (bool): Add plank scales to the forward face. """ for a, b in zip(points, points[1:]): delta = tuple(b[i] - a[i] for i in range(3)) steps = max(abs(d) for d in delta) axis = 'xyz'[max(range(3), key=lambda i: abs(delta[i]))] for step in range(steps + 1): p = tuple(round(a[i] + delta[i] * step / max(steps, 1)) for i in range(3)) for dx in range(-radius, radius + 1): for dy in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if radius and abs(dx) + abs(dy) + abs(dz) > 2 * radius: continue x, y, z = p[0] + dx, p[1] + dy, p[2] + dz if not self.inside(x, y, z): continue wood = 'minecraft:dark_oak_log' if dx == 0 or (axis == 'x' and dy == 0): wood = 'minecraft:stripped_dark_oak_log' if front and dz == -radius and dy <= 0: self.put(x, y, z, 'minecraft:dark_oak_planks') else: self.put(x, y, z, wood, axis=axis) def roots(self) -> None: """Spread thick roots down the hill and curl three broken tips skyward.""" # Hollowing follows this step, so these timbers become the room envelope. for y in range(10, 18): for x in range(8, 17): for z in range(9, 17): if ((x - 12) / (4.6 - max(0, y - 14) * 0.3))**2 + ((z - 12.5) / 4.2)**2 <= 1: wood = 'minecraft:stripped_dark_oak_log' if x in (10, 13) else 'minecraft:dark_oak_log' self.put(x, y, z, wood, axis='y') # Two sweeping front roots, each with a raised, hooked toe. self.root([(11, 16, 10), (9, 14, 9), (7, 12, 8), (6, 10, 7), (4, 8, 6), (3, 6, 5), (2, 4, 4)], front=True) self.root([(2, 4, 4), (1, 4, 3), (1, 5, 3)], radius=0, front=True) self.root([(14, 16, 10), (16, 14, 9), (17, 12, 8), (19, 10, 7), (21, 8, 6), (22, 6, 5), (22, 4, 4)], front=True) self.root([(22, 4, 4), (23, 4, 3), (23, 5, 3)], radius=0, front=True) # Front buttresses remain stout and rooted in the ground. self.root([(6, 10, 8), (5, 7, 7), (5, 2, 6), (4, 2, 4)], front=True) self.root([(18, 11, 8), (19, 7, 7), (20, 2, 6), (21, 2, 4)], front=True) # Side and rear ribs run down the slope, leaving mossy stone panels. for nodes in [ [(9, 15, 13), (6, 13, 13), (4, 10, 12), (3, 6, 11), (2, 2, 10), (1, 3, 9)], [(15, 15, 13), (18, 13, 13), (20, 10, 13), (22, 6, 12), (23, 2, 11)], [(10, 15, 15), (8, 13, 17), (6, 9, 18), (5, 5, 19), (4, 2, 20)], [(14, 15, 15), (16, 13, 17), (18, 9, 18), (20, 5, 19), (21, 2, 20)], [(12, 16, 15), (12, 13, 18), (12, 9, 20), (13, 4, 21)], ]: self.root(nodes) # Asymmetrical hooked crowns; air between the prongs defines the silhouette. self.root([(10, 16, 12), (8, 18, 12), (8, 20, 12)], front=True) self.box((8, 21, 11, 10, 21, 13), 'minecraft:dark_oak_planks') self.box((9, 22, 11, 11, 22, 12), 'minecraft:dark_oak_slab', type='bottom', waterlogged='false') self.root([(14, 16, 13), (17, 18, 13), (18, 20, 13)], front=True) self.box((16, 21, 12, 18, 21, 14), 'minecraft:dark_oak_planks') self.box((15, 22, 12, 17, 22, 13), 'minecraft:dark_oak_slab', type='bottom', waterlogged='false') self.root([(7, 12, 13), (4, 14, 14), (4, 16, 14)], front=True) self.box((4, 17, 13, 6, 17, 15), 'minecraft:dark_oak_slab', type='bottom', waterlogged='false') # Stepped grain bands follow the fan, without turning it into a roof plane. for x, y, z, facing in [(3, 7, 4, 'east'), (4, 9, 5, 'east'), (6, 11, 6, 'east'), (8, 14, 7, 'east'), (17, 14, 7, 'west'), (19, 12, 6, 'west'), (21, 10, 5, 'west'), (22, 8, 4, 'west')]: self.put(x, y, z, 'minecraft:dark_oak_stairs', facing=facing, half='bottom', shape='straight', waterlogged='false') self.put(x, y - 1, z, 'minecraft:dark_oak_planks') def rooms(self) -> None: """Hollow polygonal rooms and a two-block-wide staircase through the hill.""" # Ground floor, including a modest stone perimeter beneath the whole room. for z, (left, right) in MAIN_ROWS.items(): for x in range(left - 1, right + 2): self.put(x, 2, z, self.rng.choice(FLOOR)) if x in (left - 1, right + 1): for y in range(3, 8): if self.get(x, y, z) in (None, AIR): self.put(x, y, z, self.rng.choice(ROCK)) for x in range(left, right + 1): self.box((x, 3, z, x, 6, z), AIR) self.put(x, 7, z, 'minecraft:spruce_planks') # Stone surrounds the front and back ends of the carved room. self.texture((7, 3, 8, 18, 7, 8), ROCK) self.texture((8, 3, 19, 17, 7, 19), ROCK) # A taller vaulted loft fits inside the trunk and its broad shoulder. for z, (left, right) in LOFT_ROWS.items(): for x in range(left, right + 1): self.put(x, 7, z, 'minecraft:spruce_planks') self.box((x, 8, z, x, 11, z), AIR) ceiling = 12 if 9 <= x <= 16 and z <= 16: self.put(x, 12, z, AIR) ceiling = 13 if 10 <= x <= 14 and z <= 15: self.put(x, 13, z, AIR) ceiling = 14 if 11 <= x <= 13 and z <= 15: self.put(x, 14, z, AIR) ceiling = 15 if self.get(x, ceiling, z) in (None, AIR): self.put(x, ceiling, z, 'minecraft:dark_oak_planks') # Unbroken perimeter seals the loft even where the natural hill recedes. loft_cells = {(x, z) for z, (a, b) in LOFT_ROWS.items() for x in range(a, b + 1)} for x, z in sorted(loft_cells): for dx, dz in CARDINAL: if (x + dx, z + dz) in loft_cells: continue for y in range(7, 12): if self.get(x + dx, y, z + dz) in (None, AIR): self.put(x + dx, y, z + dz, self.rng.choice(ROCK)) # Five full stair rises, facing the direction of ascent. for z in range(11, 16): y = z - 8 for x in (17, 18): self.box((x, 3, z, x, y, z), 'minecraft:dark_oak_planks') self.put(x, y, z, 'minecraft:spruce_stairs', facing='south', half='bottom', shape='straight', waterlogged='false') self.box((x, y + 1, z, x, max(y + 3, 6), z), AIR) self.box((17, 7, 16, 18, 7, 16), 'minecraft:spruce_planks') self.box((17, 8, 16, 18, 10, 16), AIR) # Thick roots emerge as interior posts and crossbeams. for x, z in [(7, 10), (6, 16), (17, 9), (17, 18)]: self.box((x, 3, z, x, 6, z), 'minecraft:stripped_dark_oak_log', axis='y') self.box((7, 6, 10, 16, 6, 10), 'minecraft:dark_oak_log', axis='x') self.box((7, 6, 16, 16, 6, 16), 'minecraft:dark_oak_log', axis='x') for z in (11, 16): self.box((8, 8, z, 8, 11, z), 'minecraft:stripped_dark_oak_log', axis='y') self.box((16, 11, z, 16, 12, z), 'minecraft:dark_oak_log', axis='y') self.box((9, 12, z, 10, 12, z), 'minecraft:dark_oak_log', axis='x') self.box((14, 12, z, 16, 12, z), 'minecraft:dark_oak_log', axis='x') # Sturdy stairwell balustrade, kept beside the two-wide passage. for z in range(12, 16): self.put(16, 8, z, 'minecraft:dark_oak_fence', north='true', south='true', east='false', west='false', waterlogged='false') self.put(16, 7, z, 'minecraft:dark_oak_log', axis='z') # Two small daylight windows, framed in stripped timber. for x in range(10, 15): for y in range(3, 7): self.put(x, y, 19, 'minecraft:stripped_dark_oak_log', axis='z') self.box((11, 4, 19, 13, 5, 19), 'minecraft:light_gray_stained_glass') self.box((11, 4, 20, 13, 5, 21), AIR) for z in range(12, 16): for y in range(3, 7): self.put(5, y, z, 'minecraft:stripped_dark_oak_log', axis='x') self.box((5, 4, 13, 5, 5, 14), 'minecraft:light_gray_stained_glass') self.box((6, 4, 13, 6, 5, 14), AIR) self.box((1, 4, 13, 4, 5, 14), AIR) def front(self) -> None: """Set a recessed double doorway, small porch, and round trunk window.""" # The open cave vestibule is outside the closed door envelope. self.box((10, 3, 6, 14, 5, 7), AIR) self.box((11, 6, 7, 13, 6, 7), 'minecraft:dark_oak_log', axis='x') for x in (10, 13): self.box((x, 3, 8, x, 5, 8), 'minecraft:stripped_dark_oak_log', axis='y') self.box((10, 6, 8, 13, 6, 8), 'minecraft:dark_oak_log', axis='x') for x, hinge in ((11, 'left'), (12, 'right')): for y, half in ((3, 'lower'), (4, 'upper')): self.put(x, y, 8, 'minecraft:spruce_door', facing='north', hinge=hinge, half=half, open='false', powered='false') self.put(x, 5, 8, 'minecraft:brown_stained_glass') for x in (9, 15): self.put(x, 2, 5, 'minecraft:dark_oak_log', axis='y') for y in range(3, 6): self.put(x, y, 5, 'minecraft:spruce_fence', north='false', south='false', east='false', west='false', waterlogged='false') self.box((9, 6, 5, 15, 6, 7), 'minecraft:spruce_planks') self.box((9, 6, 4, 15, 6, 4), 'minecraft:spruce_slab', type='top', waterlogged='false') for x in range(9, 16): self.put(x, 7, 4, 'minecraft:spruce_trapdoor', facing='north', half='bottom', open='false', powered='false', waterlogged='false') self.put(12, 5, 5, 'minecraft:lantern', hanging='true', waterlogged='false') # Five-block octagonal frame with a cross-shaped glazed aperture. for dy in range(-2, 3): for dx in range(-2, 3): if abs(dx) + abs(dy) > 3: continue x, y = 12 + dx, 13 + dy if abs(dx) + abs(dy) <= 1: self.put(x, y, 9, 'minecraft:light_gray_stained_glass') self.put(x, y, 10, AIR) else: self.put(x, y, 9, 'minecraft:stripped_spruce_log', axis='z') for x, y, facing, half in [(10, 12, 'east', 'bottom'), (10, 14, 'east', 'top'), (14, 12, 'west', 'bottom'), (14, 14, 'west', 'top')]: self.put(x, y, 8, 'minecraft:dark_oak_stairs', facing=facing, half=half, shape='straight', waterlogged='false') self.box((11, 10, 8, 13, 10, 8), 'minecraft:dark_oak_slab', type='top', waterlogged='false') def container(self, position: Position, block: str, facing: str, items: list[tuple[str, int]]) -> None: """Place a usable storage block and record its Java 1.21 item inventory. Args: position (Position): XYZ location. block (str): Namespaced container block. facing (str): Front direction. items (list[tuple[str, int]]): Item names and counts, one stack per slot. """ self.put(*position, block, facing=facing) x, y, z = position self.block_entities.append(Compound({ 'id': String(block), 'x': Int(x), 'y': Int(y), 'z': Int(z), 'Items': NBTList[Compound]([Compound({ 'Slot': Byte(slot), 'id': String('minecraft:' + item), 'count': Int(count) }) for slot, (item, count) in enumerate(items)]), })) def furnish(self) -> None: """Fit a kitchen, hearth, workbench, dining corner, and sleeping loft.""" # A stone hearth embedded in the rear wall, with a closed stone flue. self.box((8, 2, 16, 10, 2, 18), 'minecraft:stone_bricks') for x in (8, 10): self.box((x, 3, 17, x, 4, 17), 'minecraft:cobblestone') self.put(9, 3, 17, 'minecraft:campfire', facing='north', lit='true', signal_fire='false', waterlogged='false') self.box((8, 5, 17, 10, 5, 17), 'minecraft:stone_bricks') self.box((9, 4, 18, 9, 14, 18), 'minecraft:cobblestone') self.put(9, 15, 18, 'minecraft:campfire', facing='north', lit='true', signal_fire='false', waterlogged='false') for dx, dz, facing in [(0, -1, 'south'), (0, 1, 'north'), (-1, 0, 'east'), (1, 0, 'west')]: self.put(9 + dx, 14, 18 + dz, 'minecraft:cobblestone') self.put(9 + dx, 15, 18 + dz, 'minecraft:spruce_trapdoor', facing=facing, half='bottom', open='true', powered='false', waterlogged='false') self.put(9, 16, 18, 'minecraft:stone_brick_slab', type='top', waterlogged='false') self.put(6, 3, 15, 'minecraft:smoker', facing='east', lit='false') self.put(6, 3, 14, 'minecraft:furnace', facing='east', lit='false') self.container((7, 3, 17), 'minecraft:barrel', 'north', [('bread', 12), ('baked_potato', 16), ('sweet_berries', 20)]) self.put(7, 4, 17, 'minecraft:flower_pot') self.put(6, 4, 15, 'minecraft:stone_pressure_plate', powered='false') # Workbench and shelves built into the left stone recess. self.put(6, 3, 11, 'minecraft:crafting_table') self.put(6, 3, 12, 'minecraft:loom', facing='east') self.box((6, 3, 10, 6, 4, 10), 'minecraft:bookshelf') self.put(6, 5, 10, 'minecraft:lantern', hanging='false', waterlogged='false') self.container((6, 3, 9), 'minecraft:barrel', 'east', [('iron_axe', 1), ('iron_pickaxe', 1), ('oak_sapling', 8)]) # Low table and two facing chairs keep the central passage generous. self.box((11, 3, 12, 12, 3, 12), 'minecraft:spruce_slab', type='top', waterlogged='false') self.put(10, 3, 12, 'minecraft:spruce_stairs', facing='east', half='bottom', shape='straight', waterlogged='false') self.put(13, 3, 12, 'minecraft:spruce_stairs', facing='west', half='bottom', shape='straight', waterlogged='false') self.put(11, 4, 12, 'minecraft:candle', candles='2', lit='true', waterlogged='false') for x in range(11, 15): for z in range(14, 17): self.put(x, 3, z, 'minecraft:brown_carpet' if z in (14, 16) else 'minecraft:green_carpet') for x in (14, 15): self.container((x, 3, 18), 'minecraft:barrel', 'north', [('dark_oak_log', 32), ('coal', 24), ('wheat', 16)]) self.container((16, 3, 18), 'minecraft:chest', 'north', [('leather_boots', 1), ('torch', 32), ('iron_ingot', 8)]) self.put(14, 4, 18, 'minecraft:lantern', hanging='false', waterlogged='false') self.put(12, 5, 16, 'minecraft:lantern', hanging='true', waterlogged='false') self.put(15, 5, 10, 'minecraft:lantern', hanging='true', waterlogged='false') self.put(16, 6, 14, 'minecraft:dark_oak_log', axis='x') self.put(16, 5, 14, 'minecraft:lantern', hanging='true', waterlogged='false') # Upstairs: paired moss-green beds, writing nook, and personal storage. for x in (9, 10): self.put(x, 8, 14, 'minecraft:green_bed', facing='south', part='foot', occupied='false') self.put(x, 8, 15, 'minecraft:green_bed', facing='south', part='head', occupied='false') self.container((11, 8, 15), 'minecraft:barrel', 'up', [('book', 4), ('apple', 6)]) self.put(11, 9, 15, 'minecraft:lantern', hanging='false', waterlogged='false') self.box((9, 8, 17, 10, 9, 17), 'minecraft:bookshelf') self.container((14, 8, 17), 'minecraft:chest', 'north', [('white_wool', 8), ('map', 1), ('compass', 1)]) self.put(15, 8, 17, 'minecraft:barrel', facing='north', open='false') self.put(15, 9, 17, 'minecraft:potted_fern') self.box((11, 8, 10, 13, 8, 10), 'minecraft:spruce_slab', type='top', waterlogged='false') self.put(11, 9, 10, 'minecraft:lantern', hanging='false', waterlogged='false') self.put(13, 9, 10, 'minecraft:potted_red_mushroom') self.put(12, 8, 11, 'minecraft:spruce_stairs', facing='north', half='bottom', shape='straight', waterlogged='false') for x in (12, 13, 14): for z in (13, 14, 15): self.put(x, 8, z, 'minecraft:green_carpet') # A wall-mounted landing light rests on its bracket, above walking height. self.put(17, 11, 16, 'minecraft:dark_oak_planks') self.put(17, 10, 16, 'minecraft:lantern', hanging='true', waterlogged='false') def garden(self) -> None: """Plant the roots and arrange small outdoor household props.""" # A leaf crown grows from the smaller left branch. for x in range(2, 8): for y in range(11, 16): for z in range(10, 16): if ((x - 4.7) / 3)**2 + ((y - 12.7) / 2.4)**2 + ((z - 12.5) / 3.1)**2 < 1: if self.get(x, y, z) in (None, AIR): self.put(x, y, z, 'minecraft:oak_leaves', persistent='true', distance='1', waterlogged='false') # Tiny soil pockets make the berries and ferns physically supported. for x, z, plant in [(7, 8, 'sweet_berry_bush'), (8, 8, 'sweet_berry_bush'), (7, 10, 'sweet_berry_bush'), (6, 9, 'fern'), (8, 12, 'sweet_berry_bush'), (16, 15, 'fern'), (19, 16, 'fern'), (4, 17, 'fern')]: ys = [y for y in range(2, 20) if self.get(x, y, z) not in (None, AIR) and 'leaves' not in self.get(x, y, z)] if not ys: continue y = max(ys) self.put(x, y, z, 'minecraft:moss_block') self.put(x, y + 1, z, 'minecraft:' + plant, **({'age': '3'} if plant == 'sweet_berry_bush' else {})) # Vines cling to existing timber faces on the left shoulder. for x in (4, 5, 6, 7): for y in range(9, 16): for z in range(6, 15): support = self.get(x, y, z + 1) or AIR if ('log' in support or 'planks' in support or 'leaves' in support) and self.get(x, y, z) in (None, AIR): self.put(x, y, z, 'minecraft:vine', south='true', north='false', west='false', east='false', up='false') break # Barrel, a water pail, composter, and three cut-log seats. self.container((18, 2, 3), 'minecraft:barrel', 'up', [('bucket', 1), ('bone_meal', 12), ('sweet_berries', 16)]) self.put(19, 2, 3, 'minecraft:water_cauldron', level='3') # A real bucket in an item frame hangs on the barrel above the wash pail. self.entities.append(Compound({ 'id': String('minecraft:item_frame'), 'Pos': NBTList[Double]([Double(18.5), Double(2.5), Double(2.96875)]), 'Motion': NBTList[Double]([Double(0), Double(0), Double(0)]), 'Rotation': NBTList[Float]([Float(180), Float(0)]), 'TileX': Int(18), 'TileY': Int(2), 'TileZ': Int(2), 'Facing': Byte(2), 'ItemRotation': Byte(0), 'Fixed': Byte(1), 'Invisible': Byte(0), 'Invulnerable': Byte(1), 'UUID': IntArray([self.rng.randint(-2147483648, 2147483647) for _ in range(4)]), 'Item': Compound({ 'id': String('minecraft:bucket'), 'count': Int(1) }), })) self.put(6, 2, 3, 'minecraft:composter', level='7') for x, z in [(7, 3), (16, 3), (8, 5)]: self.put(x, 2, z, 'minecraft:dark_oak_log', axis='y') self.put(7, 3, 3, 'minecraft:spruce_slab', type='bottom', waterlogged='false') self.put(16, 3, 3, 'minecraft:spruce_slab', type='bottom', waterlogged='false') for x, z in [(3, 2), (5, 3), (7, 1), (9, 1), (15, 1), (17, 2), (21, 2), (22, 7), (2, 13), (4, 19), (18, 20), (22, 18), (7, 20)]: if self.get(x, 2, z) in (None, AIR): self.put(x, 1, z, 'minecraft:grass_block') self.put(x, 2, z, self.rng.choice(['minecraft:fern', 'minecraft:short_grass'])) for x, z in [(3, 16), (21, 15)]: if self.get(x, 2, z) in (None, AIR) and self.get(x, 3, z) in (None, AIR): self.put(x, 1, z, 'minecraft:grass_block') self.put(x, 2, z, 'minecraft:large_fern', half='lower') self.put(x, 3, z, 'minecraft:large_fern', half='upper') for x, y, z in [(7, 2, 6), (17, 2, 6), (21, 2, 8), (3, 2, 7)]: if self.get(x, y, z) in (None, AIR): self.put(x, y - 1, z, 'minecraft:moss_block') self.put(x, y, z, 'minecraft:azalea') def orient_front(self) -> None: """Reflect the plan so the leaf crown appears left of the north-facing door.""" placed = list(self.placed.items()) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed = {} for (x, y, z), state in placed: name, _, suffix = state.partition('[') props = dict(item.split('=') for item in suffix.rstrip(']').split(',') if item) if props.get('facing') in ('east', 'west'): props['facing'] = {'east': 'west', 'west': 'east'}[props['facing']] if 'east' in props and 'west' in props: props['east'], props['west'] = props['west'], props['east'] if 'hinge' in props: props['hinge'] = {'left': 'right', 'right': 'left'}[props['hinge']] if props.get('shape', '').endswith(('_left', '_right')): part, side = props['shape'].rsplit('_', 1) props['shape'] = part + '_' + {'left': 'right', 'right': 'left'}[side] self.put(self.size_x - 1 - x, y, z, name, **props) for entity in self.block_entities: entity['x'] = Int(self.size_x - 1 - int(entity['x'])) for entity in self.entities: entity['TileX'] = Int(self.size_x - 1 - int(entity['TileX'])) entity['Pos'][0] = Double(self.size_x - float(entity['Pos'][0])) def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) root = schematic.write_to_nbt() region = next(iter(root['Regions'].values())) region['TileEntities'] = NBTList[Compound](self.block_entities) region['Entities'] = NBTList[Compound](self.entities) stream = io.BytesIO() NBTFile(root).write(stream) path.write_bytes(gzip.compress(stream.getvalue(), mtime=0)) return path def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") if __name__ == "__main__": main()
root-house/render.mjs — 74 行
// Render five exterior inspection views of the Root House. // Lighting, camera framing and background are already tuned; run it as is to get a first look. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 1000); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [47, 52, 59, 255]; const VIEWS = [ { name: 'front', azimuth: 210, elevation: 18, radius: 3.3 }, { name: 'entrance', azimuth: 180, elevation: 6, radius: 3.2 }, { 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})`); }
sandstone-cottage/generate.py — 558 行
# Generate Willowbank Cottage with furnished rooms, terrain, waterfront and a usable boat. # Run with /opt/venv/bin/python generate.py from any writable directory. from __future__ import annotations import argparse import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Compound, String, Double, Float, Byte, Int, IntArray, List as NBTList from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (32, 24, 26), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Willowbank Cottage", "author": "generator", "description": "A furnished sandstone-roof cottage above a river, with a sunken workshop, brick stair and timber jetty.", } PLASTER = ["minecraft:smooth_sandstone"] * 8 + ["minecraft:sandstone"] * 2 + ["minecraft:cut_sandstone"] STONE = ["minecraft:stone_bricks"] * 5 + ["minecraft:andesite"] * 2 + ["minecraft:mossy_stone_bricks"] FLOOR = ["minecraft:spruce_planks"] * 7 + ["minecraft:oak_planks"] BRICK = ["minecraft:bricks"] * 7 + ["minecraft:granite", "minecraft:polished_granite"] Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) return self.canvas.material(name, **dict(sorted(properties.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def stair(self, x: int, y: int, z: int, block: str, facing: str, half: str = "bottom") -> None: """Place a stair with explicit geometry and dry state.""" self.put(x, y, z, block, facing=facing, half=half, shape="straight", waterlogged="false") def slab(self, x: int, y: int, z: int, block: str, kind: str = "bottom") -> None: """Place a slab with explicit height.""" self.put(x, y, z, block, type=kind, waterlogged="false") def door(self, x: int, y: int, z: int, facing: str = "north", hinge: str = "left") -> None: """Place matching closed door halves on an existing floor.""" for half, dy in (("lower", 0), ("upper", 1)): self.put(x, y + dy, z, "minecraft:spruce_door", facing=facing, hinge=hinge, half=half, open="false", powered="false") def lantern(self, x: int, y: int, z: int, hanging: bool = False) -> None: """Place a lantern and remember its lighting location.""" self.put(x, y, z, "minecraft:lantern", hanging=str(hanging).lower(), waterlogged="false") def terrain(self) -> None: """Sculpt the rising riverbank, with exposed stone below the turf.""" for x in range(self.size_x): for z in range(self.size_z): if z <= 3: self.put(x, 0, z, self.rng.choice(["minecraft:gravel", "minecraft:clay", "minecraft:sand"])) self.put(x, 1, z, "minecraft:water", level="0") continue edge = 1 if x < 3 or x > 29 else 0 h = 2 if z == 4 else 3 + max(0, z - 10) // 3 h = min(8, h + (edge if z > 10 else 0)) self.texture((x, 0, z, x, max(0, h - 2), z), ["minecraft:stone"] * 12 + ["minecraft:andesite", "minecraft:tuff"]) self.texture((x, max(1, h - 1), z, x, h - 1, z), ["minecraft:dirt"] * 4 + ["minecraft:coarse_dirt"]) self.put(x, h, z, "minecraft:grass_block", snowy="false") self.ground[(x, z)] = h # Compact jetty, with posts driven to the riverbed. for x in range(11, 16): for z in range(0, 5): self.slab(x, 2, z, "minecraft:spruce_slab" if (x + z) % 4 else "minecraft:oak_slab", "top") for x, z in ((11, 0), (15, 0), (11, 3), (15, 3)): self.box((x, 0, z, x, 2, z), "minecraft:stripped_spruce_log", axis="y") self.put(15, 3, 0, "minecraft:spruce_fence", north="false", south="false", east="false", west="false", waterlogged="false") self.lantern(15, 4, 0) # A small block-built skiff keeps the boat silhouette visible in previews. for x in range(5, 9): for keel_z in range(3): self.slab(x, 1, keel_z, "minecraft:dark_oak_slab", "top") for z in (0, 2): self.put(x, 2, z, "minecraft:spruce_trapdoor", facing="south" if z == 0 else "north", half="bottom", open="true", powered="false", waterlogged="false") for x in (5, 8): self.stair(x, 2, 1, "minecraft:spruce_stairs", "west" if x == 5 else "east") self.slab(7, 2, 1, "minecraft:spruce_slab") def structure(self) -> None: """Frame two floors and the forward gabled bay around clear room volumes.""" for bounds in ((4, 3, 12, 27, 4, 22), (9, 3, 9, 16, 4, 12)): self.texture(bounds, STONE) # The two joined shells are cleared as a union after wall construction. for bounds in ((4, 5, 12, 27, 13, 22), (9, 5, 9, 16, 13, 12)): self.texture(bounds, PLASTER) self.box((5, 5, 13, 26, 13, 21), "minecraft:air") self.box((10, 5, 10, 15, 13, 13), "minecraft:air") # Warm boards on both living surfaces, interrupted later by the stairwell. for y in (4, 9): self.texture((5, y, 13, 26, y, 21), FLOOR) self.texture((10, y, 10, 15, y, 13), FLOOR) for x, z in ((4, 12), (9, 12), (16, 12), (22, 12), (27, 12), (4, 22), (10, 22), (16, 22), (22, 22), (27, 22), (9, 9), (16, 9)): self.box((x, 4, z, x, 13, z), "minecraft:stripped_spruce_log", axis="y") self.put(x, 9, z, "minecraft:stripped_oak_log", axis="z") for y in (9, 13): for z in (12, 22): self.box((4, y, z, 27, y, z), "minecraft:stripped_spruce_log", axis="x") for x in (4, 27): self.box((x, y, 12, x, y, 22), "minecraft:stripped_spruce_log", axis="z") self.box((9, y, 9, 16, y, 9), "minecraft:stripped_spruce_log", axis="x") # Reopen the join where the bay meets the main house. self.box((10, 5, 12, 15, 8, 12), "minecraft:air") self.box((10, 10, 12, 15, 13, 12), "minecraft:air") self.texture((10, 9, 12, 15, 9, 12), FLOOR) # Ceiling provides an unbroken weather seal, independent of stair models. self.box((4, 14, 12, 27, 14, 22), "minecraft:spruce_planks") # Lower workshop and store connected through a broad, two-high arch. self.box((16, 5, 13, 16, 8, 21), "minecraft:stripped_spruce_log", axis="y") self.box((16, 5, 17, 16, 7, 18), "minecraft:air") # Floor-to-floor stair: two lanes, five risers, full overhead clearance. self.box((25, 5, 14, 26, 13, 18), "minecraft:air") for z in range(14, 19): y = z - 9 for x in (25, 26): self.box((x, 4, z, x, y - 1, z), "minecraft:spruce_planks") self.stair(x, y, z, "minecraft:spruce_stairs", "south") for z in range(14, 18): self.put(24, 10, z, "minecraft:spruce_fence", north=str(z > 14).lower(), south=str(z < 17).lower(), west="false", east="false", waterlogged="false") self.door(19, 10, 12, hinge="left") self.door(20, 10, 12, hinge="right") self.door(12, 5, 9) self.put(12, 4, 8, "minecraft:stone_bricks") self.stair(12, 3, 7, "minecraft:stone_brick_stairs", "south") # Glass blocks make the small cellar lights weather-tight. for a, b, y0, y1, z in ((6, 7, 6, 6, 12), (24, 25, 6, 6, 12), (6, 7, 10, 12, 12), (24, 25, 10, 12, 12), (12, 13, 10, 15, 9), (6, 8, 10, 12, 22), (18, 20, 10, 12, 22)): for x in range(a, b + 1): for y in range(y0, y1 + 1): self.put(x, y, z, "minecraft:glass") for x in (a - 1, b + 1): if not (z == 9 and x in (11, 14)): self.box((x, y0, z, x, y1, z), "minecraft:stripped_oak_log", axis="y") for x in (4, 27): self.box((x, 10, 16, x, 12, 18), "minecraft:glass") for z in (15, 19): self.box((x, 10, z, x, 12, z), "minecraft:stripped_oak_log", axis="y") # Beam ends and deep eaves echo the reference's dark horizontal line. for z in (11, 23): self.box((3, 13, z, 28, 13, z), "minecraft:dark_oak_log", axis="x") for x in (3, 28): self.box((x, 13, 11, x, 13, 23), "minecraft:dark_oak_log", axis="z") for x in (5, 8, 18, 21, 23, 26): self.stair(x, 12, 11, "minecraft:dark_oak_stairs", "south", "top") # Dark shutters flank the tall wing windows without covering the glass. for x in (5, 8, 23, 26): for y in (10, 11, 12): self.put(x, y, 11, "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false") def roof(self) -> None: """Join the broad main roof with the forward cross-gable.""" surfaces = {} for x in range(3, 29): for z in range(10, 25): y = 14 + min(z - 10, 24 - z) surfaces[(x, z)] = (y, "south" if z <= 17 else "north", x in (3, 28) or z in (10, 24), "main") for x in range(8, 18): for z in range(8, 18): y = 14 + min(x - 8, 17 - x) old = surfaces.get((x, z)) if old is None or y >= old[0]: surfaces[(x, z)] = (y, "east" if x <= 12 else "west", z == 8 or x in (8, 17), "gable") # Plaster triangles on the two ends and the front of the projecting bay. for x in (4, 27): for z in range(12, 23): roof_y = surfaces[(x, z)][0] if roof_y > 15: self.texture((x, 15, z, x, roof_y - 1, z), PLASTER) for x in range(9, 17): cap = 14 + min(x - 8, 17 - x) self.texture((x, 14, 9, x, cap - 1, 9), PLASTER) for x in (9, 16): for z in (10, 11): self.box((x, 14, z, x, surfaces[(x, z)][0] - 1, z), "minecraft:smooth_sandstone") for (x, z), (y, facing, dark, roof_type) in surfaces.items(): self.put(x, y - 1, z, "minecraft:dark_oak_planks" if dark else "minecraft:smooth_sandstone") self.stair(x, y, z, "minecraft:dark_oak_stairs" if dark else "minecraft:smooth_sandstone_stairs", facing) if roof_type == "main" and z == 17: self.put(x, y, z, "minecraft:dark_oak_log", axis="x") self.slab(x, y + 1, z, "minecraft:dark_oak_slab") if roof_type == "gable" and x in (12, 13): self.slab(x, y + 1, z, "minecraft:dark_oak_slab") # Smooth sandstone ridge-side caps break up the pale field subtly. for x in (5, 6, 24, 25): self.slab(x, 20, 16, "minecraft:smooth_sandstone_slab", "double") self.box((12, 10, 9, 13, 15, 9), "minecraft:glass") for x in (11, 14): self.box((x, 10, 9, x, 15, 9), "minecraft:stripped_oak_log", axis="y") # Chimney is backed by solid masonry and capped against the sky. self.texture((6, 10, 19, 7, 22, 20), BRICK) self.box((6, 23, 19, 7, 23, 20), "minecraft:brick_slab", type="bottom", waterlogged="false") # Decorative attic vents are backed by plaster, so remain sealed. for x, face in ((3, "west"), (28, "east")): self.put(x, 17, 17, "minecraft:dark_oak_trapdoor", facing=face, half="bottom", open="true", powered="false", waterlogged="false") def exterior(self) -> None: """Add the brick stair, slab flower shelves and planted riverbank.""" for z in range(5, 12): y = z - 2 for x in range(18, 22): self.texture((x, 2, z, x, y - 1, z), BRICK) self.stair(x, y, z, "minecraft:brick_stairs", "south") for x in (17, 22): self.texture((x, 2, z, x, y - 1, z), STONE) self.slab(x, y, z, "minecraft:polished_granite_slab") for x in (18, 21): self.slab(x, 9, 11, "minecraft:brick_slab", "top") # A mixed path forks toward the cellar and climbs from the jetty. path_cells = set() for z in range(4, 7): for x in range(12 + (z - 4), 20): path_cells.add((x, z)) path_cells.update((x, z) for z in (6, 7) for x in range(11, 16)) for x, z in sorted(path_cells): if 17 <= x <= 22 and z >= 5: continue y = self.ground[(x, z)] self.put(x, y, z, self.rng.choice(["minecraft:dirt_path"] * 4 + ["minecraft:coarse_dirt", "minecraft:gravel", "minecraft:andesite"])) # Small stairs make the river-to-bank elevation change walkable. for x in range(12, 16): self.stair(x, 3, 5, "minecraft:stone_brick_stairs", "south") for a, b in ((4, 8), (23, 27)): for x in range(a, b + 1): self.slab(x, 8, 10, "minecraft:smooth_sandstone_slab", "top") self.put(x, 8, 11, "minecraft:rooted_dirt") self.put(x, 9, 11, self.rng.choice(["minecraft:oxeye_daisy", "minecraft:cornflower", "minecraft:azure_bluet"])) self.put(x, 8, 9, "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false") for x in (a, b): self.stair(x, 7, 11, "minecraft:spruce_stairs", "south", "top") # Door lamps stand on supported bracket blocks beside the openings. for x in (18, 21): self.put(x, 11, 11, "minecraft:dark_oak_fence", north="false", south="true", east="false", west="false", waterlogged="false") self.lantern(x, 12, 11) self.put(15, 7, 8, "minecraft:spruce_fence", north="false", south="true", east="false", west="false", waterlogged="false") self.lantern(15, 8, 8) # Deliberate flower beds, clear of doors and stair approaches. for x, z in [(x, 10) for x in range(4, 9)] + [(x, 10) for x in range(24, 29)]: self.put(x, 3, z, "minecraft:rooted_dirt") self.put(x, 4, z, "minecraft:sweet_berry_bush", age="3") for x, z in ((3, 10), (8, 9), (9, 7), (16, 8), (23, 9), (28, 11)): y = self.ground.get((x, z), 3) self.put(x, y + 1, z, "minecraft:azalea") if x in (3, 28): self.put(x, y + 2, z, "minecraft:oak_leaves", persistent="true", distance="1", waterlogged="false") # Boundary planting gives the slope a natural, uneven edge. for (x, z), y in sorted(self.ground.items()): if self.get(x, y, z) != "minecraft:grass_block[snowy=false]" or self.get(x, y + 1, z) not in (None, "minecraft:air"): continue if (4 <= x <= 28 and z >= 8) or (10 <= x <= 22 and z <= 8): continue roll = self.rng.random() if roll < 0.15 and y + 2 < self.size_y and self.get(x, y + 2, z) is None: self.put(x, y + 1, z, "minecraft:tall_grass", half="lower") self.put(x, y + 2, z, "minecraft:tall_grass", half="upper") elif roll < 0.45: self.put(x, y + 1, z, "minecraft:short_grass") elif roll < 0.52: self.put(x, y + 1, z, "minecraft:oxeye_daisy") # One small birch off the left rear corner frames the cottage. self.box((1, 8, 21, 1, 13, 21), "minecraft:birch_log", axis="y") for x in range(0, 3): for z in range(19, 24): for y in range(12, 16): if abs(x - 1) + abs(z - 21) + max(0, y - 13) <= 3 and self.get(x, y, z) in (None, "minecraft:air"): self.put(x, y, z, "minecraft:birch_leaves", persistent="true", distance="1", waterlogged="false") def interiors(self) -> None: """Furnish living, dining, sleeping and cellar corners around open routes.""" # Hearth in the rear left, backed by the chimney stack. self.texture((5, 9, 18, 8, 9, 21), STONE) for x in (5, 8): self.box((x, 10, 20, x, 12, 20), "minecraft:bricks") self.put(6, 10, 20, "minecraft:campfire", facing="north", lit="true", signal_fire="false", waterlogged="false") self.put(7, 10, 20, "minecraft:campfire", facing="north", lit="true", signal_fire="false", waterlogged="false") self.box((5, 12, 20, 8, 12, 20), "minecraft:bricks") for x in range(5, 9): self.slab(x, 12, 19, "minecraft:dark_oak_slab", "top") self.put(5, 13, 20, "minecraft:potted_fern") self.put(8, 13, 20, "minecraft:candle", candles="3", lit="true", waterlogged="false") for x in range(7, 10): self.stair(x, 10, 15, "minecraft:spruce_stairs", "south") for x in (6, 10): self.put(x, 10, 15, "minecraft:spruce_planks") self.put(6, 11, 15, "minecraft:potted_azalea_bush") self.box((7, 10, 16, 10, 10, 18), "minecraft:green_carpet") for x in (8, 9): self.slab(x, 10, 17, "minecraft:dark_oak_slab", "top") self.put(8, 11, 17, "minecraft:candle", candles="2", lit="true", waterlogged="false") for z in (13, 14): self.put(5, 10, z, "minecraft:bookshelf") self.lantern(5, 11, 14) # Reading and sleeping nook inside the projecting bay. self.put(10, 10, 10, "minecraft:cyan_bed", facing="south", part="foot", occupied="false") self.put(10, 10, 11, "minecraft:cyan_bed", facing="south", part="head", occupied="false") self.put(15, 10, 10, "minecraft:barrel", facing="north", open="false") self.lantern(15, 11, 10) self.put(12, 10, 11, "minecraft:light_gray_carpet") self.put(13, 10, 11, "minecraft:light_gray_carpet") # Kitchen at the back, well away from the stairwell along the east wall. for x in range(17, 23): self.put(x, 10, 21, "minecraft:barrel", facing="north", open="false") self.put(18, 10, 21, "minecraft:smoker", facing="north", lit="false") self.put(19, 10, 21, "minecraft:water_cauldron", level="3") self.put(20, 10, 21, "minecraft:crafting_table") self.put(17, 11, 21, "minecraft:potted_red_tulip") self.lantern(22, 11, 21) # Solid slab table on legs, with chairs facing inward. for x in (18, 20): self.put(x, 10, 17, "minecraft:dark_oak_fence", north="false", south="false", east="false", west="false", waterlogged="false") for x in range(18, 21): self.slab(x, 11, 17, "minecraft:spruce_slab", "top") self.put(19, 12, 17, "minecraft:candle", candles="3", lit="true", waterlogged="false") for x in (18, 20): self.stair(x, 10, 16, "minecraft:dark_oak_stairs", "south") self.stair(x, 10, 18, "minecraft:dark_oak_stairs", "north") self.box((18, 10, 13, 21, 10, 14), "minecraft:brown_carpet") # Beam-mounted lamps leave the entrance and circulation routes clear. for x, z in ((13, 17), (21, 15), (24, 20)): self.lantern(x, 13, z, hanging=True) self.box((11, 13, 17, 15, 13, 17), "minecraft:stripped_spruce_log", axis="x") self.lantern(13, 12, 17, hanging=True) # Workbench row, storage, and a clear landing in the cellar. for x in range(5, 10): self.put(x, 5, 21, "minecraft:barrel", facing="north", open="false") self.put(6, 5, 21, "minecraft:crafting_table") self.put(7, 5, 21, "minecraft:smithing_table") self.put(8, 5, 21, "minecraft:stonecutter", facing="north") self.lantern(5, 6, 21) self.put(9, 6, 21, "minecraft:potted_cactus") for x in (6, 8): self.put(x, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false") self.put(5, 5, 17, "minecraft:barrel", facing="east", open="false") self.lantern(5, 6, 17) self.put(14, 5, 10, "minecraft:barrel", facing="north", open="false") self.lantern(14, 6, 10) self.box((11, 5, 15, 13, 5, 17), "minecraft:brown_carpet") for x in (18, 19, 21, 22): self.put(x, 5, 21, "minecraft:barrel", facing="north", open="false") self.put(x, 6, 21, "minecraft:barrel", facing="north", open="false") self.put(18, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false") self.put(21, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false") self.put(23, 5, 21, "minecraft:crafting_table") self.lantern(23, 6, 21) self.put(23, 5, 13, "minecraft:barrel", facing="north", open="false") self.lantern(23, 6, 13) self.box((18, 8, 17, 23, 8, 17), "minecraft:stripped_spruce_log", axis="x") self.lantern(20, 7, 17, hanging=True) self.lantern(12, 8, 19, hanging=True) self.box((10, 8, 16, 14, 8, 16), "minecraft:stripped_spruce_log", axis="x") self.lantern(11, 7, 16, hanging=True) def build(self) -> "Build": """Build the complete cottage, hillside, waterfront and furnishings.""" self.ground = {} self.terrain() self.structure() self.roof() self.exterior() self.interiors() return self def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) nbt = schematic.write_to_nbt() # Actual usable Java 1.21.1 boat, kept beside the visible timber skiff. boat = Compound({ "id": String("minecraft:boat"), "Type": String("spruce"), "Pos": NBTList[Double]([Double(3.0), Double(1.55), Double(1.8)]), "Motion": NBTList[Double]([Double(0), Double(0), Double(0)]), "Rotation": NBTList[Float]([Float(90), Float(0)]), "UUID": IntArray([145995982, 77563907, 642301188, 23812227]), "OnGround": Byte(0), "Invulnerable": Byte(0), }) nbt["Regions"][CONFIG["name"]]["Entities"] = NBTList[Compound]([boat]) NBTFile(nbt).save(path) return path def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") if __name__ == "__main__": main()
sandstone-cottage/render.mjs — 73 行
// Starting point for rendering a build. Copy into the workspace and edit VIEWS. // Lighting, camera framing and background are already tuned; run it as is to get a first look. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 768); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [47, 52, 59, 255]; const VIEWS = [ { name: 'front', azimuth: 200, elevation: 18, radius: 3.2 }, { 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})`); }
stone-cottage/generate.py — 407 行
# Generate and verify a furnished stone cottage for Minecraft Java 1.21.1. from __future__ import annotations import argparse import json import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 from mcio.nbt.nbt import File as NBTFile # noqa: E402 from mcio.nbt.tag import Byte, Compound, Int, List as NBTList, Short, String # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (16, 15, 14), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Sunflower Stone Cottage", "author": "generator", "description": "Compact stone and oak cottage, recessed north entry, two side windows, furnished room and ladder loft.", } WALL = ["minecraft:andesite"] * 5 + ["minecraft:stone"] * 3 + ["minecraft:cobblestone"] * 2 BASE = ["minecraft:cobblestone"] * 4 + ["minecraft:mossy_cobblestone"] * 2 + ["minecraft:andesite"] ROOF = ["minecraft:andesite_stairs"] * 6 + ["minecraft:cobblestone_stairs"] * 3 + ["minecraft:stone_stairs"] TIMBER = "minecraft:stripped_oak_log" TRIM = "minecraft:spruce_planks" HOUSE = (3, 3, 11, 11) # x0, z0, x1, z1 WINDOW_Z = range(6, 9) LOFT_BOUNDS = (5, 6, 8, 9, 6, 10) LADDER_XZ = (7, 10) BLOCK_REGISTRY = json.loads(Path("/work/generator/MCRender/resources/1.21.1-blocks.json").read_text()) Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) allowed, defaults = BLOCK_REGISTRY[name.removeprefix("minecraft:")] complete = dict(defaults, **properties) for key, value in complete.items(): assert key in allowed and value in allowed[key], f"invalid block state: {name} {key}={value}" return self.canvas.material(name, **dict(sorted(complete.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def build(self) -> "Build": """Build the reference cottage and a compact, accessible furnished interior. Returns: Build: This instance, so calls can be chained. """ self.ground_and_walls() self.roof() self.openings() self.interior() self.garden() return self def ground_and_walls(self) -> None: """Place grass, a continuous floor, stone panels and the exposed frame.""" self.box((0, 0, 0, self.size_x - 1, 0, self.size_z - 1), "minecraft:grass_block") self.texture((3, 1, 3, 11, 1, 11), BASE) self.texture((4, 1, 4, 10, 1, 10), ["minecraft:oak_planks"] * 4 + [TRIM]) for x in range(3, 12): for z in range(3, 12): if x in (3, 11) or z in (3, 11): self.texture((x, 2, z, x, 5, z), WALL) if self.rng.random() < 0.3: self.put(x, 2, z, self.rng.choice(BASE)) for x in (3, 11): for z in (3, 11): self.box((x, 1, z, x, 6, z), TIMBER, axis="y") self.box((x, 6, 3, x, 6, 11), TIMBER, axis="z") for z in (3, 11): self.box((4, 6, z, 10, 6, z), TIMBER, axis="x") # One small inlaid floor runner keeps the main path level and unobstructed. self.box((6, 1, 6, 8, 1, 8), "minecraft:terracotta") self.box((7, 1, 6, 7, 1, 8), "minecraft:brown_terracotta") def roof(self) -> None: """Raise one pale gable with a continuous solid backing and wooden verge.""" for x in range(3, 12): underside = 11 - abs(x - 7) for z in (3, 11): self.texture((x, 7, z, x, underside, z), WALL) for z in (3, 11): self.box((7, 7, z, 7, 11, z), TIMBER, axis="y") self.box((5, 8, z, 9, 8, z), TIMBER, axis="x") for x in (2, 12): self.box((x, 6, 2, x, 6, 12), TIMBER, axis="z") for x in range(2, 13): height = 12 - abs(x - 7) for z in range(2, 13): verge = z in (2, 12) self.put(x, height - 1, z, TRIM if verge else self.rng.choice(WALL)) if x in (2, 12): self.put(x, height - 1, z, TIMBER, axis="z") if x == 7: self.put(x, height, z, "minecraft:spruce_slab" if verge else "minecraft:andesite_slab", type="bottom") else: self.put(x, height, z, "minecraft:spruce_stairs" if verge else self.rng.choice(ROOF), facing="east" if x < 7 else "west", half="bottom", shape="straight") # Small supported corbels and end caps emphasize the reference's heavy eaves. for x in (2, 12): for z in (3, 5, 9, 11): self.put(x, 5, z, "minecraft:spruce_stairs", facing="west" if x == 2 else "east", half="top", shape="straight") self.box((7, 12, 0, 7, 12, 3), TIMBER, axis="z") self.put(7, 13, 2, "minecraft:spruce_fence") self.put(7, 11, 0, "minecraft:chain", axis="y") self.put(7, 10, 0, "minecraft:lantern", hanging="true") self.put(7, 12, 12, TIMBER, axis="z") def openings(self) -> None: """Frame two clear glazed windows and a door recessed behind the facade.""" for x in (3, 11): for z in (5, 9): self.box((x, 2, z, x, 5, z), TIMBER, axis="y") for y in (2, 5): self.box((x, y, 6, x, y, 8), TIMBER, axis="z") for z in WINDOW_Z: for y in (3, 4): self.put(x, y, z, "minecraft:glass_pane", north="true", south="true", east="false", west="false") for x in (6, 8): self.box((x, 1, 2, x, 1, 4), "minecraft:stone_bricks") self.box((x, 2, 3, x, 3, 4), TIMBER, axis="y") self.put(x, 2, 2, "minecraft:oak_fence", north="false", south="true", east="false", west="false") self.put(x, 3, 2, "minecraft:oak_fence", north="false", south="true", east="false", west="false") self.box((5, 4, 2, 9, 4, 3), TIMBER, axis="x") self.box((6, 4, 4, 8, 4, 4), TIMBER, axis="x") self.box((7, 2, 2, 7, 3, 3), "minecraft:air") self.box((7, 1, 2, 7, 1, 4), "minecraft:stone_bricks") self.put(7, 1, 1, "minecraft:stone_brick_stairs", facing="south", half="bottom", shape="straight") for half, y in (("lower", 2), ("upper", 3)): self.put(7, y, 4, "minecraft:spruce_door", half=half, facing="north", hinge="left", open="false", powered="false") self.box((5, 5, 1, 5, 5, 3), TIMBER, axis="z") self.put(5, 4, 1, "minecraft:lantern", hanging="true") self.put(9, 5, 2, "minecraft:spruce_slab", type="bottom") def interior(self) -> None: """Furnish one main room and a small rear loft reached by a fixed ladder.""" self.texture((4, 1, 4, 5, 1, 5), ["minecraft:stone_bricks", "minecraft:andesite"]) self.put(4, 2, 4, "minecraft:furnace", facing="east", lit="false") self.put(4, 2, 5, "minecraft:smoker", facing="east", lit="false") self.put(4, 2, 9, "minecraft:crafting_table") self.box((4, 4, 4, 4, 4, 5), "minecraft:stone_brick_stairs", facing="west", half="top", shape="straight") self.put(4, 3, 4, "minecraft:stone_bricks") self.put(4, 2, 10, "minecraft:barrel", facing="east", open="false") self.put(4, 3, 10, "minecraft:potted_fern") self.put(10, 2, 10, "minecraft:barrel", facing="north", open="false") for part, z in (("foot", 9), ("head", 10)): self.put(9, 2, z, "minecraft:red_bed", facing="south", part=part, occupied="false") self.put(9, 2, 6, "minecraft:spruce_stairs", facing="east", half="top", shape="straight") self.put(10, 2, 6, "minecraft:spruce_stairs", facing="west", half="top", shape="straight") self.put(9, 2, 5, "minecraft:oak_stairs", facing="north", half="bottom", shape="straight") self.put(9, 3, 6, "minecraft:potted_dandelion") self.box(LOFT_BOUNDS, TRIM) self.box((3, 6, 7, 11, 6, 7), TIMBER, axis="x") for x in range(5, 10): self.put(x, 7, 8, "minecraft:spruce_fence", east="true" if x < 9 else "false", west="true" if x > 5 else "false", north="false", south="false") for y in range(2, 8): self.put(7, y, 10, "minecraft:ladder", facing="north") self.put(5, 7, 10, "minecraft:barrel", facing="east", open="false") self.put(5, 7, 9, "minecraft:bookshelf") self.put(9, 7, 10, "minecraft:barrel", facing="north", open="false") self.put(7, 5, 7, "minecraft:chain", axis="y") self.put(7, 4, 7, "minecraft:lantern", hanging="true") self.put(5, 5, 10, "minecraft:chain", axis="y") self.put(5, 4, 10, "minecraft:lantern", hanging="true") self.put(8, 9, 10, "minecraft:lantern", hanging="true") def garden(self) -> None: """Add the sunflower planter, supported greenery, hay and a short path.""" for z in WINDOW_Z: self.put(12, 1, z, "minecraft:grass_block") self.put(13, 1, z, "minecraft:spruce_trapdoor", facing="east", half="bottom", open="true", powered="false") for z, facing in ((5, "north"), (9, "south")): self.put(12, 1, z, "minecraft:spruce_trapdoor", facing=facing, half="bottom", open="true", powered="false") for z in (6, 7): self.put(12, 2, z, "minecraft:cornflower") self.put(12, 2, 8, "minecraft:sunflower", half="lower") self.put(12, 3, 8, "minecraft:sunflower", half="upper") # Leaves touch frame or connected leaves; persistent prevents decay. leaves = [(13, 6, 3), (12, 5, 3), (12, 4, 3), (12, 5, 4), (13, 5, 3), (13, 6, 11), (12, 5, 11), (12, 4, 11), (13, 5, 11), (12, 5, 10), (2, 5, 3), (2, 4, 3), (2, 5, 4), (1, 6, 3), (2, 2, 10), (2, 1, 10), (2, 1, 11)] for p in leaves: self.put(*p, "minecraft:oak_leaves", persistent="true", distance="1") for y in (2, 3, 4): self.put(12, y, 4, "minecraft:vine", west="true", east="false", north="false", south="false", up="false") for y in (2, 3): self.put(2, y, 4, "minecraft:vine", east="true", west="false", north="false", south="false", up="false") for x, y, z in [(14, 1, 10), (15, 1, 10), (14, 1, 11), (15, 1, 11), (14, 2, 11)]: self.put(x, y, z, "minecraft:hay_block", axis="y" if y == 2 else "x") path = [(6, 0, 0), (7, 0, 0), (8, 0, 0), (6, 0, 1), (8, 0, 1), (6, 0, 2), (8, 0, 2)] for p in path: self.put(*p, self.rng.choice(["minecraft:gravel", "minecraft:coarse_dirt", "minecraft:andesite"])) tufts = [(1, 1, 1), (3, 1, 1), (10, 1, 1), (12, 1, 2), (14, 1, 4), (14, 1, 7), (15, 1, 8), (13, 1, 12), (10, 1, 13), (8, 1, 13), (3, 1, 13), (1, 1, 8), (1, 1, 5)] for p in tufts: self.put(*p, "minecraft:short_grass") for p in [(2, 1, 6), (4, 1, 12), (15, 1, 5)]: self.put(*p, "minecraft:dandelion") def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) nbt = schematic.write_to_nbt() region = next(iter(nbt["Regions"].values())) region["TileEntities"] = self.block_entities() NBTFile(nbt).save(path) return path def block_entities(self) -> NBTList: """Create bed and container NBT, including a few useful cottage supplies. Returns: NBTList: Block entities with region-relative coordinates. """ entities = [] supplies = {(4, 2, 10): ("minecraft:bread", 6), (10, 2, 10): ("minecraft:coal", 12), (5, 7, 10): ("minecraft:wheat_seeds", 16), (9, 7, 10): ("minecraft:oak_log", 8)} for (x, y, z), state in sorted(self.placed.items()): name = state.split("[")[0].removeprefix("minecraft:") kind = "bed" if name.endswith("_bed") else name if kind not in ("bed", "furnace", "smoker", "barrel"): continue tag = Compound({"id": String(f"minecraft:{kind}"), "x": Int(x), "y": Int(y), "z": Int(z)}) if kind != "bed": items = [] if (x, y, z) in supplies: item_id, count = supplies[(x, y, z)] items.append(Compound({"Slot": Byte(0), "id": String(item_id), "count": Int(count)})) tag["Items"] = NBTList[Compound](items) if kind in ("furnace", "smoker"): tag.update({"BurnTime": Short(0), "CookTime": Short(0), "CookTimeTotal": Short(200), "RecipesUsed": Compound()}) entities.append(tag) return NBTList[Compound](entities) def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") if __name__ == "__main__": main()
stone-cottage/render.mjs — 73 行
// Starting point for rendering a build. Copy into the workspace and edit VIEWS. // Lighting, camera framing and background are already tuned; run it as is to get a first look. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 768); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [47, 52, 59, 255]; const VIEWS = [ { name: 'front', azimuth: 145, elevation: 18, radius: 3.3 }, { name: 'rear', azimuth: 325, elevation: 22, radius: 3.3 }, { name: 'side', azimuth: 100, 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})`); }
red-roof-farmstead/generate.py — 589 行
# Starting point for a Minecraft build generator. Copy into the workspace and fill in build(). # Everything that has to be right for the harness is already right here: bounds checked placement, # one seeded random source, and a byte reproducible export. from __future__ import annotations import argparse import gzip import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (42, 29, 42), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Red Roof Farmstead", "author": "generator", "description": "Timber farmhouse, furnished loft, well, wheat garden and four-sail windmill. Java 1.21.1.", } Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) return self.canvas.material(name, **dict(sorted(properties.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def p(self, x: int, y: int, z: int, block: str, **properties: str) -> None: """Place a short vanilla block name at XYZ, forwarding its properties.""" self.put(x, y, z, 'minecraft:' + block, **properties) def fill(self, bounds: Tuple[int, int, int, int, int, int], block: str, **properties: str) -> None: """Fill inclusive XYZ bounds using a short vanilla block name.""" self.box(bounds, 'minecraft:' + block, **properties) def mix(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Texture inclusive XYZ bounds from short vanilla block names.""" self.texture(bounds, ['minecraft:' + name for name in palette]) @staticmethod def roof_height(z: int) -> int: """Return the stepped main roof height at a north/south coordinate.""" return 9 + min(z - 5, 24 - z) def terrain(self) -> None: """Build the flat square meadow, stone plinth, and branching farm track.""" self.mix((0, 0, 0, 41, 0, 41), ['stone', 'stone', 'andesite', 'cobblestone']) self.fill((0, 1, 0, 41, 1, 41), 'dirt') self.fill((0, 2, 0, 41, 2, 41), 'grass_block') for x in range(42): for z in range(42): if x in (0, 41) or z in (0, 41): self.p(x, 1, z, self.rng.choice(['stone_bricks', 'stone_bricks', 'mossy_stone_bricks'])) self.p(x, 2, z, 'stone_brick_slab', type='bottom') # Widened yard narrows into a slightly meandering approach to the gate. self.path_cells = set() for z in range(23, 42): center = 18 if z < 31 else 19 width = 4 if z < 29 else 3 for x in range(center - width // 2, center + width // 2 + 2): self.path_cells.add((x, z)) for x in range(8, 29): for z in range(30, 33): self.path_cells.add((x, z)) for x, z in sorted(self.path_cells): self.p(x, 2, z, self.rng.choice(['dirt_path'] * 7 + ['coarse_dirt'] * 3 + ['packed_mud', 'rooted_dirt'])) # Gate posts and the low front boundary keep the square plot legible. for x0, x1 in [(2, 15), (24, 39)]: for x in range(x0, x1 + 1): self.p(x, 3, 39, 'spruce_fence') for x in (x0, x1): self.fill((x, 3, 39, x, 4, 39), 'oak_log', axis='y') self.p(x, 5, 39, 'spruce_slab', type='bottom') for x in (17, 22): self.fill((x, 3, 39, x, 4, 39), 'oak_log', axis='y') self.p(x, 5, 39, 'lantern', hanging='false') # An open four-block gate leaves the farm lane traversable. for x in (18, 19, 20, 21): self.p(x, 3, 39, 'spruce_fence_gate', facing='south', open='true', in_wall='false') def house(self) -> None: """Build the timber shell, sealed red roof, glazed dormer, and cupola.""" self.mix((9, 3, 7, 28, 3, 22), ['stone_bricks'] * 5 + ['cobblestone', 'mossy_stone_bricks']) self.mix((10, 3, 8, 27, 3, 21), ['oak_planks'] * 8 + ['spruce_planks']) for x in range(9, 29): for z in range(7, 23): if x in (9, 28) or z in (7, 22): for y in range(4, self.roof_height(z)): self.p(x, y, z, self.rng.choice(['oak_planks'] * 8 + ['stripped_oak_log', 'spruce_planks'])) for x in (9, 28): for z in (7, 14, 15, 22): self.fill((x, 4, z, x, self.roof_height(z) - 1, z), 'spruce_log', axis='y') for z in (7, 22): for x in (13, 16, 21, 24): self.fill((x, 4, z, x, 10, z), 'oak_log', axis='y') self.fill((9, 8, z, 28, 8, z), 'spruce_log', axis='x') self.fill((9, 10, z, 28, 10, z), 'spruce_log', axis='x') for x in (9, 28): self.fill((x, 8, 7, x, 8, 22), 'spruce_log', axis='z') self.fill((x, 10, 7, x, 10, 22), 'spruce_log', axis='z') # Double full-block roof backing seals the staircase profile against weather. for z in range(5, 25): h = self.roof_height(z) for x in range(7, 31): self.p(x, h - 1, z, 'red_terracotta') self.p(x, h, z, self.rng.choice(['mangrove_planks'] * 8 + ['red_nether_bricks', 'red_terracotta'])) if z not in (14, 15): self.p(x, h + 1, z, 'mangrove_stairs' if self.rng.random() < .90 else 'red_nether_brick_stairs', facing='south' if z < 14 else 'north', half='bottom', shape='straight') else: self.p(x, h + 1, z, 'red_nether_brick_slab', type='bottom') for x in (7, 30): self.p(x, h, z, 'smooth_sandstone') self.p(x, h + 1, z, 'smooth_sandstone_stairs', facing='south' if z < 14 else 'north', half='bottom', shape='straight') # Timber eaves and corbels ground the wide overhang. for z in (5, 24): self.fill((8, 8, z, 29, 8, z), 'spruce_planks') for x in (9, 13, 16, 21, 24, 28): self.p(x, 8, 23, 'spruce_stairs', facing='north', half='top', shape='straight') self.p(x, 8, 6, 'spruce_stairs', facing='south', half='top', shape='straight') self.fill((10, 9, 8, 27, 9, 21), 'spruce_planks') # Glazed, inset openings stay clear of the fireplace and the ladder. for z in (7, 22): for x0 in (11, 25): self.fill((x0, 5, z, x0 + 1, 7, z), 'gray_stained_glass') self.fill((x0, 4, z, x0 + 1, 4, z), 'stripped_oak_log', axis='x') for x in (9, 28): for z0 in (10, 16): self.fill((x, 5, z0, x, 7, z0 + 1), 'gray_stained_glass') self.fill((x, 4, z0, x, 4, z0 + 1), 'stripped_oak_log', axis='z') self.fill((x, 12, 14, x, 14, 15), 'gray_stained_glass') self.fill((x, 11, 13, x, 11, 16), 'stripped_oak_log', axis='z') # Tall shadowed surround and complete paired doors. for x in (17, 20): self.fill((x, 4, 22, x, 7, 22), 'spruce_log', axis='y') self.fill((17, 7, 22, 20, 7, 22), 'dark_oak_log', axis='x') for x, hinge in [(18, 'left'), (19, 'right')]: for y, half in [(4, 'lower'), (5, 'upper')]: self.p(x, y, 22, 'dark_oak_door', facing='south', half=half, hinge=hinge, open='false', powered='false') self.p(x, 6, 22, 'dark_oak_planks') self.fill((17, 3, 23, 20, 3, 23), 'stone_brick_stairs', facing='north', half='bottom', shape='straight') # Dormer projects from the front slope, centered over the doorway. self.fill((15, 9, 18, 22, 9, 23), 'spruce_planks') for x in range(15, 23): h = 12 + min(x - 14, 23 - x) self.fill((x, 10, 23, x, h - 1, 23), 'oak_planks') if x in (15, 22): self.fill((x, 10, 18, x, h - 1, 23), 'oak_log', axis='y') for x in range(14, 24): h = 12 + min(x - 14, 23 - x) for z in range(18, 25): if h < self.roof_height(z): continue self.fill((x, h - 1, z, x, h, z), 'red_nether_bricks') self.p(x, h + 1, z, 'red_nether_brick_stairs', facing='east' if x < 19 else 'west', half='bottom', shape='straight') self.p(x, h, 24, 'smooth_sandstone') self.p(x, h + 1, 24, 'smooth_sandstone_stairs', facing='east' if x < 19 else 'west', half='bottom', shape='straight') # Carve only the inner dormer union, preserving its two-block roof backing. for x in range(16, 22): h = 12 + min(x - 14, 23 - x) self.fill((x, 10, 18, x, h - 2, 22), 'air') self.fill((17, 11, 23, 20, 13, 23), 'gray_stained_glass') self.fill((16, 10, 23, 21, 10, 23), 'stripped_oak_log', axis='x') for x in (15, 22): self.fill((x, 8, 23, x, 12, 23), 'oak_log', axis='y') # Decorative cupola is solid-cored, glazed, and lit, with a layered pyramid cap. self.fill((16, 17, 12, 21, 18, 17), 'spruce_planks') self.fill((16, 19, 12, 21, 20, 17), 'gray_stained_glass') self.fill((17, 19, 13, 20, 20, 16), 'glowstone') for x in (16, 21): for z in (12, 17): self.fill((x, 18, z, x, 20, z), 'oak_log', axis='y') self.fill((15, 21, 11, 22, 21, 18), 'smooth_sandstone') for x in range(15, 23): for z in range(11, 19): if x in (15, 22) or z in (11, 18): self.p(x, 22, z, 'smooth_sandstone_slab', type='bottom') for y, inset in [(22, 0), (23, 1), (24, 2)]: self.fill((16 + inset, y, 12 + inset, 21 - inset, y, 17 - inset), 'red_nether_bricks') self.fill((18, 25, 14, 19, 25, 15), 'red_nether_brick_slab', type='bottom') def furnishings(self) -> None: """Furnish both accessible floors with farm work, eating, and sleeping areas.""" # Hearth opens south. Stone cheeks and a hood carry the exterior flue. self.mix((25, 3, 18, 27, 3, 20), ['stone_bricks', 'andesite']) self.fill((25, 4, 18, 27, 6, 18), 'stone_bricks') for x in (25, 27): self.fill((x, 4, 19, x, 6, 20), 'stone_bricks') self.fill((25, 7, 18, 27, 7, 20), 'stone_bricks') self.p(26, 4, 19, 'campfire', facing='south', lit='true', signal_fire='false') self.p(26, 4, 20, 'iron_bars', east='true', west='true', north='false', south='false') for y in range(8, 22): self.p(26, y, 19, self.rng.choice(['andesite', 'stone_bricks', 'cobblestone'])) self.p(26, 21, 19, 'chiseled_stone_bricks') self.p(26, 22, 19, 'campfire', facing='east', lit='true', signal_fire='false') # Six-seat farmhouse table with grounded legs and a sturdy tabletop. for x in (17, 20): for z in (14, 16): self.p(x, 4, z, 'spruce_fence') self.fill((17, 5, 14, 20, 5, 16), 'oak_slab', type='top') for x, face in [(16, 'east'), (21, 'west')]: for z in (14, 16): self.p(x, 4, z, 'spruce_stairs', facing=face, half='bottom', shape='straight') self.p(18, 4, 13, 'spruce_stairs', facing='south', half='bottom', shape='straight') self.p(19, 4, 17, 'spruce_stairs', facing='north', half='bottom', shape='straight') self.p(18, 6, 15, 'candle', candles='3', lit='true') self.p(20, 6, 15, 'flower_pot') for x in range(17, 21): for z in range(18, 21): self.p(x, 4, z, 'red_carpet' if (x + z) % 3 else 'brown_carpet') # Harvest stores, tools, and a kitchen/work bench around the open room. for x, z in [(10, 19), (10, 20), (12, 20), (13, 20), (10, 9), (10, 10), (24, 9)]: self.p(x, 4, z, 'barrel', facing='up', open='false') self.p(10, 5, 20, 'barrel', facing='south', open='false') self.fill((10, 4, 17, 11, 4, 18), 'hay_block', axis='y') self.p(10, 5, 18, 'hay_block', axis='x') self.p(11, 4, 8, 'crafting_table') self.p(12, 4, 8, 'smoker', facing='south', lit='false') self.p(13, 4, 8, 'chest', facing='south', type='single') self.p(14, 4, 8, 'furnace', facing='south', lit='true') self.p(15, 4, 8, 'water_cauldron', level='3') self.fill((13, 7, 8, 15, 7, 8), 'spruce_slab', type='top') self.p(13, 8, 8, 'flower_pot') for x, z in [(13, 20), (24, 9)]: self.p(x, 5, z, 'lantern', hanging='false') for x, z in [(12, 11), (22, 13), (15, 17)]: self.p(x, 8, z, 'chain', axis='y') self.p(x, 7, z, 'lantern', hanging='true') # Ladder has continuous wall backing and a two-block clear landing. self.p(10, 9, 14, 'air') for y in range(4, 11): self.p(10, y, 14, 'ladder', facing='east') for z in (13, 15): self.p(11, 10, z, 'spruce_fence') for x in (12, 13, 24): for z, part in [(13, 'foot'), (12, 'head')]: self.p(x, 10, z, 'red_bed', facing='north', part=part, occupied='false') for x in range(14, 17): for z in range(12, 16): self.p(x, 10, z, 'white_carpet' if (x + z) % 4 else 'red_carpet') self.p(11, 10, 10, 'chest', facing='south', type='single') self.p(11, 10, 17, 'barrel', facing='up', open='false') self.p(11, 11, 17, 'lantern', hanging='false') self.p(23, 10, 12, 'barrel', facing='up', open='false') self.p(23, 11, 12, 'lantern', hanging='false') self.p(25, 10, 17, 'barrel', facing='up', open='false') self.p(25, 11, 17, 'lantern', hanging='false') self.fill((10, 10, 11, 10, 11, 12), 'bookshelf') self.p(10, 12, 11, 'lantern', hanging='false') self.fill((18, 10, 22, 19, 10, 22), 'oak_planks') self.p(18, 10, 20, 'spruce_stairs', facing='south', half='bottom', shape='straight') self.p(19, 11, 22, 'candle', candles='2', lit='true') self.p(18, 11, 22, 'flower_pot') self.p(20, 10, 20, 'barrel', facing='up', open='false') self.p(20, 11, 20, 'lantern', hanging='false') self.p(17, 10, 10, 'barrel', facing='up', open='false') self.p(17, 11, 10, 'lantern', hanging='false') # Two sconces flank the doorway without occupying its passage. for x in (16, 21): self.p(x, 5, 23, 'spruce_slab', type='top') self.p(x, 6, 23, 'lantern', hanging='false') def well(self) -> None: """Place a stone water well beneath an open timber shelter.""" self.mix((3, 2, 27, 9, 2, 33), ['cobblestone'] * 4 + ['andesite', 'mossy_cobblestone']) self.mix((4, 3, 28, 8, 4, 32), ['stone_bricks'] * 3 + ['cobblestone', 'mossy_stone_bricks']) self.fill((5, 3, 29, 7, 4, 31), 'water', level='0') for x in range(4, 9): for z in range(28, 33): if x in (4, 8) or z in (28, 32): self.p(x, 5, z, 'stone_brick_slab', type='bottom') for x in (3, 9): for z in (27, 33): self.p(x, 3, z, 'cobblestone') self.fill((x, 4, z, x, 8, z), 'spruce_fence') self.p(x, 9, z, 'oak_log', axis='y') for z in (27, 33): self.fill((3, 8, z, 9, 8, z), 'spruce_log', axis='x') self.fill((3, 8, 27, 3, 8, 33), 'spruce_log', axis='z') self.fill((9, 8, 27, 9, 8, 33), 'spruce_log', axis='z') self.mix((2, 9, 26, 10, 9, 34), ['oak_planks'] * 5 + ['spruce_planks']) self.fill((3, 10, 27, 9, 10, 33), 'oak_slab', type='bottom') for x in (2, 10): for z in (26, 34): self.p(x, 10, z, 'spruce_fence') self.fill((3, 7, 30, 9, 7, 30), 'oak_log', axis='x') self.p(6, 6, 30, 'chain', axis='y') self.p(6, 5, 30, 'lantern', hanging='true') self.p(10, 3, 32, 'water_cauldron', level='2') def field_and_mill(self) -> None: """Build the irrigated wheat patch, harvest yard, and four-armed mill.""" for x in range(26, 38): for z in range(28, 38): if x in (26, 37) or z in (28, 37): if (x, z) not in [(26, 31), (26, 32)]: self.p(x, 3, z, self.rng.choice(['cobblestone', 'andesite', 'stone_bricks'])) else: self.p(x, 2, z, 'farmland', moisture='7') self.p(x, 3, z, 'wheat', age='7' if self.rng.random() < .86 else '5') for z in range(29, 37): self.p(31, 2, z, 'water', level='0') self.p(31, 3, z, 'air') for z in (30, 35): self.fill((30, 3, z, 32, 3, z), 'oak_slab', type='bottom') self.p(36, 2, 33, 'water', level='0') self.p(36, 3, 33, 'oak_slab', type='bottom') for x, z in [(26, 28), (37, 28), (37, 37), (26, 37)]: self.p(x, 4, z, 'cobblestone_slab', type='bottom') self.p(x + (-1 if x == 26 else 1), 3, z, 'andesite_stairs', facing='east' if x == 26 else 'west', half='bottom', shape='straight') # Paddock fence and logs, with gateways to mill and crop bed. for z in range(20, 38): self.p(39, 3, z, 'spruce_fence') for x in range(32, 40): self.p(x, 3, 20, 'spruce_fence') for x, z in [(32, 20), (39, 20), (39, 27), (39, 37)]: self.fill((x, 3, z, x, 4, z), 'oak_log', axis='y') self.p(x, 5, z, 'spruce_slab', type='bottom') self.p(35, 3, 20, 'spruce_fence_gate', facing='south', open='true', in_wall='false') self.p(36, 3, 20, 'spruce_fence_gate', facing='south', open='true', in_wall='false') self.fill((30, 3, 23, 32, 3, 25), 'hay_block', axis='y') self.fill((31, 4, 23, 32, 4, 24), 'hay_block', axis='x') self.p(32, 5, 23, 'hay_block', axis='z') self.p(30, 3, 26, 'barrel', facing='up', open='false') self.p(30, 4, 26, 'lantern', hanging='false') # Solid tapering timber tower stands five blocks clear of the house wall. self.mix((34, 3, 10, 37, 3, 13), ['stone_bricks', 'cobblestone']) self.fill((34, 4, 10, 37, 7, 13), 'spruce_planks') for x in (34, 37): for z in (10, 13): self.fill((x, 4, z, x, 9, z), 'oak_log', axis='y') self.fill((34, 8, 10, 37, 8, 13), 'oak_log', axis='x') self.fill((35, 9, 11, 36, 14, 12), 'stripped_spruce_log', axis='y') self.fill((34, 15, 10, 37, 15, 13), 'spruce_slab', type='bottom') self.fill((35, 13, 12, 35, 13, 15), 'oak_log', axis='z') # Four offset lattice paddles create a clearly readable pinwheel silhouette. for d in range(-5, 6): if d: self.p(35 + d, 13, 15, 'stripped_oak_log', axis='x') self.p(35, 13 + d, 15, 'stripped_oak_log', axis='y') for d in range(3, 6): for w in (1, 2): for x, y in [(35 + w, 13 + d), (35 + d, 13 - w), (35 - w, 13 - d), (35 - d, 13 + w)]: self.p(x, y, 15, 'oak_fence') self.p(35, 13, 16, 'stripped_oak_log', axis='z') self.p(35, 4, 14, 'grindstone', face='floor', facing='south') self.p(35, 3, 14, 'cobblestone') self.p(36, 3, 15, 'barrel', facing='up', open='false') self.p(36, 4, 15, 'lantern', hanging='false') def greenery(self) -> None: """Scatter supported meadow plants, flower patches, and low berry hedges.""" patches = [(3, 4), (5, 18), (3, 36), (12, 35), (28, 39), (39, 5), (38, 24), (22, 4)] for cx, cz in patches: for dx, dz in [(-1, 0), (0, 0), (1, 0), (0, 1), (1, 1)]: x, z = cx + dx, cz + dz if self.get(x, 3, z) in (None, 'minecraft:air') and self.get(x, 2, z) == 'minecraft:grass_block': self.p(x, 3, z, 'oak_leaves', persistent='true', distance='1') for dx, dz in [(-1, 1), (0, -1), (2, 0), (0, 2)]: x, z = cx + dx, cz + dz if self.get(x, 3, z) in (None, 'minecraft:air') and self.get(x, 2, z) == 'minecraft:grass_block': self.p(x, 3, z, self.rng.choice(['poppy', 'oxeye_daisy', 'cornflower', 'dandelion'])) for x in range(2, 40): for z in range(2, 40): if self.get(x, 2, z) != 'minecraft:grass_block' or self.get(x, 3, z) not in (None, 'minecraft:air'): continue # Exclude circulation strips around every built feature. if (7 <= x <= 30 and 5 <= z <= 25) or (1 <= x <= 11 and 25 <= z <= 35) or (32 <= x <= 38 and 9 <= z <= 21): continue t = self.rng.random() if t < .18: self.p(x, 3, z, 'short_grass') elif t < .225 and self.get(x, 4, z) is None: self.p(x, 3, z, 'tall_grass', half='lower') self.p(x, 4, z, 'tall_grass', half='upper') elif t < .245: self.p(x, 3, z, self.rng.choice(['poppy', 'oxeye_daisy', 'cornflower'])) for x, z in [(5, 7), (4, 20), (23, 36), (38, 7)]: if self.get(x, 3, z) in (None, 'minecraft:air', 'minecraft:short_grass'): self.p(x, 3, z, 'mossy_cobblestone') def connect_fences(self) -> None: """Resolve all lattice and railing connections explicitly for schematic rendering.""" for (x, y, z), state in list(self.placed.items()): if state.split('[')[0] not in ('minecraft:spruce_fence', 'minecraft:oak_fence'): continue properties = {} for face, dx, dz in [('east', 1, 0), ('west', -1, 0), ('south', 0, 1), ('north', 0, -1)]: neighbor = self.get(x + dx, y, z + dz) or 'minecraft:air' name = neighbor.split('[')[0] connect = any(token in name for token in ('_fence', '_log', '_planks', 'stone_bricks', 'cobblestone')) properties[face] = str(connect).lower() self.put(x, y, z, state.split('[')[0], **properties) def build(self) -> 'Build': """Assemble the reference farmstead with deterministic detail and usable interiors.""" self.terrain() self.house() self.furnishings() self.well() self.field_and_mill() self.greenery() self.connect_fences() return self def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) schematic.save(path) # Java Litematica consumes compressed NBT. Pin gzip time as well. path.write_bytes(gzip.compress(path.read_bytes(), mtime=0)) return path def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") if __name__ == "__main__": main()
red-roof-farmstead/render.mjs — 73 行
// Starting point for rendering a build. Copy into the workspace and edit VIEWS. // Lighting, camera framing and background are already tuned; run it as is to get a first look. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 960); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [171, 178, 164, 255]; const VIEWS = [ { name: 'front', azimuth: 35, elevation: 28, radius: 3.4 }, { name: 'rear', azimuth: 215, elevation: 25, radius: 3.4 }, { name: 'side', azimuth: 305, elevation: 23, radius: 3.4 }, { name: 'top', azimuth: 35, elevation: 60, 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})`); }
stilt-water-cabin/generate.py — 459 行
# Generate Reedwater Cabin and validate a deterministic single-region Java 1.21.1 export. from __future__ import annotations import argparse import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.schematic import load_schematic # noqa: E402 from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Byte, Compound, Double, Float, Int, IntArray, List as NBTList, String from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (28, 21, 24), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Reedwater Cabin", "author": "generator", "description": "Oak stilt fisherman cabin with spruce deck, dark oak gable, furnished room, shallow pond and oak boat.", } # Water occupies y=1..2. Floor tops are y=7, landing top y=4. WATER_Y = 2 FLOOR_Y = 6 WALLS = (9, 6, 23, 16) # x0, z0, x1, z1 DOORS = ((9, 7, 10), (9, 7, 11)) BOAT_POSITION = (10.25, 2.65, 21.1) POSTS = ((9, 6), (16, 6), (23, 6), (9, 16), (16, 16), (23, 16), (9, 9), (9, 12), (23, 11)) DECK_POSTS = ((3, 5), (3, 11), (3, 17), (8, 5), (8, 17), (14, 19)) WINDOWS = tuple((x, y, z) for z in (6, 16) for x in (12, 13, 19, 20) for y in range(8, 11)) + tuple((x, y, z) for x in (9, 23) for z in (7, 8, 14, 15) for y in range(8, 11)) ROOM_BOUNDS = (10, 7, 7, 22, 11, 15) LIGHTS = ((12, 10, 9), (19, 10, 9), (12, 10, 13), (19, 10, 13)) Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} self.block_entities = [] self.entities = [] def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) return self.canvas.material(name, **dict(sorted(properties.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def build(self) -> "Build": """Build a timber fishing cabin with a navigable room and a shallow pond. Returns: Build: Completed canvas. """ self.pond() self.structure() self.deck() self.roof() self.furnish() self.plants_and_mooring() self.connect_fences() return self def pond(self) -> None: """Lay a varied silt bed, two water layers, and irregular reed banks.""" self.texture((0, 0, 0, 27, 0, 23), ["minecraft:mud"] * 5 + ["minecraft:dirt"] * 3 + ["minecraft:rooted_dirt", "minecraft:clay"]) self.box((0, 1, 0, 27, WATER_Y, 23), "minecraft:water", level="0") for x in range(28): for z in range(24): bank = z <= 1 and (x > 15 or x < 5) or x >= 26 and z < 17 fringe = (z == 2 and x in (0, 1, 3, 18, 19, 23, 24, 26, 27)) or (x == 25 and z in (1, 2, 5, 6, 12, 13, 14)) if bank or fringe: self.put(x, 1, z, "minecraft:dirt") self.put(x, 2, z, self.rng.choice(["minecraft:mud", "minecraft:mud", "minecraft:grass_block", "minecraft:rooted_dirt"])) elif self.rng.random() < 0.08: self.put(x, 1, z, "minecraft:seagrass") def structure(self) -> None: """Raise the cabin on oak piles and build the sealed timber envelope.""" # Intentional bands preserve timber framing rather than random wall noise. self.box((9, FLOOR_Y, 6, 23, FLOOR_Y, 16), "minecraft:spruce_planks") for x in range(10, 23): for z in range(7, 16): self.put(x, FLOOR_Y, z, "minecraft:oak_planks" if x % 4 else "minecraft:stripped_oak_log", **({"axis": "z"} if not x % 4 else {})) for z in (6, 16): self.box((9, 5, z, 23, 5, z), "minecraft:spruce_log", axis="x") for x in range(9, 24): self.box((x, 7, z, x, 11, z), "minecraft:stripped_oak_log" if x in (10, 15, 17, 22) else "minecraft:oak_planks", **({"axis": "y"} if x in (10, 15, 17, 22) else {})) self.box((9, 12, z, 23, 12, z), "minecraft:stripped_spruce_log", axis="x") for x in (9, 23): self.box((x, 5, 6, x, 5, 16), "minecraft:spruce_log", axis="z") self.box((x, 7, 6, x, 11, 16), "minecraft:oak_planks") self.box((x, 12, 6, x, 12, 16), "minecraft:stripped_spruce_log", axis="z") for x, z in POSTS: self.box((x, 1, z, x, 12, z), "minecraft:oak_log", axis="y") # Submerged footings are solid and visible through the shallow water. self.put(x, 0, z, "minecraft:oak_log", axis="y") for x, y, z in WINDOWS: long_wall = z in (6, 16) self.put(x, y, z, "minecraft:glass_pane", north=str(not long_wall).lower(), south=str(not long_wall).lower(), east=str(long_wall).lower(), west=str(long_wall).lower(), waterlogged="false") # Sills and shutters frame the glass without overlapping its cells. for z, face in ((5, "north"), (17, "south")): for start in (12, 19): for x in range(start, start + 2): self.put(x, 7, z, "minecraft:spruce_stairs", facing=face, half="top", shape="straight", waterlogged="false") for x in (start - 1, start + 2): for y in (8, 9, 10): self.put(x, y, z, "minecraft:spruce_trapdoor", facing=face, half="bottom", open="true", powered="false", waterlogged="false") for x, face in ((8, "west"), (24, "east")): for start in (7, 14): for z in (start, start + 1): self.put(x, 7, z, "minecraft:spruce_stairs", facing=face, half="top", shape="straight", waterlogged="false") for index, (x, y, z) in enumerate(DOORS): for dy, half in ((0, "lower"), (1, "upper")): self.put(x, y + dy, z, "minecraft:spruce_door", facing="west", half=half, hinge="left" if index == 0 else "right", open="false", powered="false") self.put(x, y + 2, z, "minecraft:oak_planks") # Full ceiling closes every roof void above the habitable room. self.box((10, 12, 7, 22, 12, 15), "minecraft:spruce_planks") for x in (12, 19): self.box((x, 12, 7, x, 12, 15), "minecraft:stripped_oak_log", axis="z") def deck(self) -> None: """Build the level wraparound deck, connected stairs, and low landing.""" self.box((3, 6, 5, 8, 6, 17), "minecraft:spruce_planks") self.box((8, 6, 17, 14, 6, 19), "minecraft:spruce_planks") self.box((8, 5, 19, 14, 5, 19), "minecraft:spruce_log", axis="x") for z in (5, 11, 17): self.box((3, 5, z, 8, 5, z), "minecraft:spruce_log", axis="x") for x in (3, 8): self.box((x, 6, 5, x, 6, 17), "minecraft:stripped_spruce_log", axis="z") for x, z in DECK_POSTS: top = 6 if (x, z) == (8, 17) else 7 self.box((x, 0, z, x, top, z), "minecraft:oak_log", axis="y") if top == 7: self.put(x, 8, z, "minecraft:spruce_slab", type="bottom", waterlogged="false") rails = {(3, z) for z in range(5, 18)} | {(x, 5) for x in range(3, 9)} | {(7, 17)} | {(x, 19) for x in range(8, 15)} | {(14, 17), (14, 18)} for x, z in sorted(rails): if (x, z) not in DECK_POSTS: self.put(x, 7, z, "minecraft:spruce_fence", waterlogged="false") for z, y in ((18, 5), (19, 4), (20, 3)): self.box((4, y, z, 6, y, z), "minecraft:spruce_stairs", facing="north", half="bottom", shape="straight", waterlogged="false") for x in (3, 7): self.box((x, 1, z, x, y, z), "minecraft:stripped_spruce_log", axis="y") self.put(x, y + 1, z, "minecraft:spruce_fence", waterlogged="false") self.box((3, 3, 21, 8, 3, 22), "minecraft:spruce_planks") for x, z in ((3, 21), (3, 22), (8, 21), (8, 22)): self.box((x, 0, z, x, 3, z), "minecraft:oak_log", axis="y") # Mooring bollard stands next to open water, outside the stair path. self.box((8, 4, 21, 8, 5, 21), "minecraft:oak_log", axis="y") self.put(8, 6, 21, "minecraft:spruce_slab", type="bottom", waterlogged="false") def roof(self) -> None: """Lay a low half-step dark-oak gable with deep, thick spruce verges.""" for z in range(4, 19): course = 7 - abs(z - 11) y = 12 + course // 2 face = "south" if z < 11 else "north" for x in range(6, 26): # Continuous plank backing closes stair and slab seams. self.put(x, y - 1, z, "minecraft:dark_oak_planks") wood = "spruce" if x in (6, 25) else "dark_oak" if z == 11: self.put(x, y, z, f"minecraft:{wood}_planks") self.put(x, y + 1, z, f"minecraft:{wood}_slab", type="bottom", waterlogged="false") elif course % 2: self.put(x, y, z, f"minecraft:{wood}_stairs", facing=face, half="bottom", shape="straight", waterlogged="false") else: self.put(x, y, z, f"minecraft:{wood}_slab", type="bottom", waterlogged="false") for x in (9, 23): if y > 12: self.box((x, 12, z, x, y - 1, z), "minecraft:oak_planks") # A second stepped verge underneath makes the gable ends substantial. for x in (6, 25): self.put(x, y - 1, z, "minecraft:spruce_planks") for x in (8, 24): self.box((x, 11, 5, x, 11, 17), "minecraft:oak_log", axis="z") for z in (5, 17): self.box((7, 11, z, 24, 11, z), "minecraft:oak_log", axis="x") for x in (6, 25): self.box((x, 16, 11, x, 17, 11), "minecraft:spruce_planks") self.put(x, 18, 11, "minecraft:spruce_slab", type="bottom", waterlogged="false") # Gable timber and knee braces emphasize the sheltered entrance. self.box((8, 12, 11, 8, 14, 11), "minecraft:stripped_oak_log", axis="y") for z, facing in ((6, "south"), (16, "north")): self.put(8, 10, z, "minecraft:spruce_stairs", facing=facing, half="top", shape="straight", waterlogged="false") for x in (7, 24): self.put(x, 11, 18, "minecraft:chain", axis="y", waterlogged="false") self.put(x, 10, 18, "minecraft:lantern", hanging="true", waterlogged="false") def inventory(self, pos: Position, block_id: str, items: Tuple[Tuple[str, int], ...]) -> None: """Store actual Java 1.21.1 gear inside a placed container. Args: pos (Position): Container XYZ coordinate. block_id (str): Block entity ID. items (Tuple[Tuple[str, int], ...]): Item names and counts in slot order. """ x, y, z = pos self.block_entities.append(Compound({"id": String(f"minecraft:{block_id}"), "x": Int(x), "y": Int(y), "z": Int(z), "Items": NBTList[Compound]([Compound({"Slot": Byte(slot), "id": String(f"minecraft:{name}"), "count": Int(count)}) for slot, (name, count) in enumerate(items)])})) def furnish(self) -> None: """Furnish the room while keeping broad circulation routes and clear windows.""" for x, y, z in LIGHTS: self.put(x, y + 1, z, "minecraft:chain", axis="y", waterlogged="false") self.put(x, y, z, "minecraft:lantern", hanging="true", waterlogged="false") # A smoker provides cooking without a spreading open fire in a timber house. for x, block in ((15, "smoker"), (16, "crafting_table"), (17, "water_cauldron"), (18, "barrel")): props = {"facing": "south", "lit": "false"} if block == "smoker" else {"level": "3"} if block == "water_cauldron" else {"facing": "south", "open": "false"} if block == "barrel" else {} self.put(x, 7, 7, f"minecraft:{block}", **props) self.inventory((15, 7, 7), "smoker", (("cod", 8), ("charcoal", 12))) self.inventory((18, 7, 7), "barrel", (("bread", 12), ("bowl", 3), ("cooked_salmon", 8))) for x, y, z in ((10, 7, 12), (10, 7, 13), (10, 8, 12), (22, 7, 9)): self.put(x, y, z, "minecraft:barrel", facing="south" if x < 20 else "west", open="false") self.inventory((x, y, z), "barrel", (("fishing_rod", 1), ("string", 16), ("cod", 6))) self.put(22, 8, 9, "minecraft:potted_spruce_sapling") for z, part in ((9, "head"), (10, "foot")): self.put(21, 7, z, "minecraft:green_bed", facing="north", part=part, occupied="false") # Shelving remains above two-block headroom and away from the glazing. for x in (15, 16, 17, 18): self.put(x, 10, 7, "minecraft:spruce_slab", type="top", waterlogged="false") self.put(16, 11, 7, "minecraft:flower_pot") for x in (19, 20): self.put(x, 7, 14, "minecraft:oak_fence", waterlogged="false") self.put(x, 8, 14, "minecraft:spruce_slab", type="top", waterlogged="false") self.put(19, 9, 14, "minecraft:potted_blue_orchid") for x, face in ((18, "east"), (21, "west")): self.put(x, 7, 14, "minecraft:spruce_stairs", facing=face, half="bottom", shape="straight", waterlogged="false") for x in range(13, 17): for z in range(10, 13): self.put(x, 7, z, "minecraft:brown_carpet" if x in (13, 16) or z in (10, 12) else "minecraft:green_carpet") self.put(4, 7, 6, "minecraft:barrel", facing="south", open="false") self.inventory((4, 7, 6), "barrel", (("fishing_rod", 1), ("lead", 2), ("salmon", 10))) self.put(6, 7, 6, "minecraft:chest", facing="south", type="single", waterlogged="false") self.inventory((6, 7, 6), "chest", (("oak_boat", 1), ("leather_boots", 1), ("kelp", 20))) # Bench under the entrance overhang, backed by the cabin wall. for z in (14, 15): self.put(5, 7, z, "minecraft:spruce_stairs", facing="west", half="bottom", shape="straight", waterlogged="false") def plants_and_mooring(self) -> None: """Add a supported wall bush, living reeds, lily pads, and the actual boat.""" self.put(16, 7, 17, "minecraft:spruce_planks") self.put(16, 7, 18, "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false") for x, y, z in ((16, 8, 17), (16, 9, 17), (16, 10, 17), (15, 9, 17), (17, 9, 17), (16, 9, 18), (15, 10, 17), (17, 10, 17)): self.put(x, y, z, self.rng.choice(("minecraft:oak_leaves", "minecraft:oak_leaves", "minecraft:azalea_leaves")), persistent="true", distance="1", waterlogged="false") # Reeds have real dirt substrate and an adjacent source-water cell. for x, z in ((1, 4), (1, 5), (2, 5), (1, 17), (2, 18), (1, 20), (18, 22), (19, 22), (21, 21), (24, 19), (25, 18), (24, 3), (20, 2), (6, 2)): self.put(x, 1, z, "minecraft:dirt") self.put(x, 2, z, "minecraft:mud") for y in range(3, self.rng.choice((5, 5, 6))): self.put(x, y, z, "minecraft:sugar_cane", age="0") for x, z in ((2, 9), (1, 13), (11, 20), (14, 22), (17, 19), (20, 18), (23, 21), (26, 20), (15, 3), (9, 2), (4, 2), (26, 22)): if self.get(x, 2, z) == "minecraft:water[level=0]": self.put(x, 3, z, "minecraft:lily_pad") # Boats are entities in Java 1.21.1. The geometry-only renderer omits them. self.entities.append(Compound({ "id": String("minecraft:boat"), "Type": String("oak"), "Pos": NBTList[Double]([Double(v) for v in BOAT_POSITION]), "Motion": NBTList[Double]([Double(0), Double(0), Double(0)]), "Rotation": NBTList[Float]([Float(90), Float(0)]), "UUID": IntArray([20260923, 1953066084, 1466000227, 1]), "Invulnerable": Byte(0), "OnGround": Byte(0), })) def connect_fences(self) -> None: """Set fence connections explicitly because schematics do not update states.""" for (x, y, z), state in list(self.placed.items()): if state.split("[")[0] not in ("minecraft:spruce_fence", "minecraft:oak_fence"): continue props = {"waterlogged": "false"} for face, dx, dz in (("east", 1, 0), ("west", -1, 0), ("south", 0, 1), ("north", 0, -1)): neighbor = self.get(x + dx, y, z + dz) or "minecraft:air" props[face] = str(any(kind in neighbor for kind in ("_fence", "_log", "_planks"))).lower() self.put(x, y, z, state.split("[")[0], **props) def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) root = schematic.write_to_nbt() region = root["Regions"][CONFIG["name"]] region["TileEntities"] = NBTList[Compound](self.block_entities) region["Entities"] = NBTList[Compound](self.entities) NBTFile(root).save(path) return path def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" root = NBTFile.load_regardless_of_gzipped(path) region = root["Regions"][CONFIG["name"]] assert list(region["Entities"]) == self.entities, "entity data changed on reload" assert list(region["TileEntities"]) == self.block_entities, "block entity data changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") print(f"verified {len(build.block_entities)} stocked block entities and {len(build.entities)} oak boat entity") if __name__ == "__main__": main()
stilt-water-cabin/render.mjs — 73 行
// Starting point for rendering a build. Copy into the workspace and edit VIEWS. // Lighting, camera framing and background are already tuned; run it as is to get a first look. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 512); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [47, 52, 59, 255]; const VIEWS = [ { name: 'front', azimuth: 305, elevation: 20, radius: 3.35 }, { name: 'rear', azimuth: 125, elevation: 24, radius: 3.35 }, { name: 'side', azimuth: 225, elevation: 16, radius: 3.35 }, { name: 'top', azimuth: 305, 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})`); }
stilted-manor/generate.py — 678 行
# Generate and verify a furnished stilted medieval manor for Minecraft Java 1.21.1. from __future__ import annotations import argparse import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (33, 39, 31), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "The Brackenwatch Manor", "author": "generator", "description": "Stilted medieval manor with workshop, great room, two bedchambers, and ridge lookout. Java 1.21.1.", } # Restrained weighted palettes keep masonry and plaster readable at this scale. STONE = ['minecraft:stone_bricks'] * 6 + ['minecraft:cobblestone'] * 3 + ['minecraft:andesite'] BASE = STONE + ['minecraft:mossy_stone_bricks'] * 3 + ['minecraft:mossy_cobblestone'] * 2 PLASTER = ['minecraft:smooth_sandstone'] * 10 + ['minecraft:cut_sandstone'] * 2 + ['minecraft:sandstone'] FLOOR = ['minecraft:spruce_planks'] * 9 + ['minecraft:dark_oak_planks'] ROOF_SOLID = ['minecraft:stone_bricks'] * 6 + ['minecraft:cobblestone'] * 2 + ['minecraft:andesite'] Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) return self.canvas.material(name, **dict(sorted(properties.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def timber(self, bounds, axis='y', dark=False): """Place structural timber with its grain along the member.""" self.box(bounds, 'minecraft:dark_oak_log' if dark else 'minecraft:stripped_spruce_log', axis=axis) def stairs(self, x, y, z, facing, block='spruce', half='bottom'): """Place a straight stair with explicit stable state.""" self.put(x, y, z, f'minecraft:{block}_stairs', facing=facing, half=half, shape='straight', waterlogged='false') def slab(self, x, y, z, block='spruce', kind='bottom'): """Place a slab with an explicit half.""" self.put(x, y, z, f'minecraft:{block}_slab', type=kind, waterlogged='false') def door(self, x, y, z, facing='north', hinge='left'): """Place both matching halves of an operable door.""" for half, dy in [('lower', 0), ('upper', 1)]: self.put(x, y + dy, z, 'minecraft:spruce_door', facing=facing, hinge=hinge, half=half, open='false', powered='false') def trap(self, x, y, z, facing, half='bottom'): """Attach a vertical spruce shutter or planter facing.""" self.put(x, y, z, 'minecraft:spruce_trapdoor', facing=facing, half=half, open='true', powered='false', waterlogged='false') def lantern(self, x, y, z, hanging=False): """Place a standing or suspended lantern.""" self.put(x, y, z, 'minecraft:lantern', hanging=str(hanging).lower(), waterlogged='false') def candle(self, x, y, z, count=3): """Place a lit candle cluster on furniture.""" self.put(x, y, z, 'minecraft:candle', candles=str(count), lit='true', waterlogged='false') def bed(self, x, y, z, facing, color): """Place a complete bed, specifying its foot coordinate.""" dx, dz = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}[facing] for part, px, pz in [('foot', x, z), ('head', x + dx, z + dz)]: self.put(px, y, pz, f'minecraft:{color}_bed', part=part, facing=facing, occupied='false') def planter(self, axis, fixed, lo, hi, y, outward): """Build a soil trough with attached wooden fascias and flowers.""" flowers = ['cornflower', 'poppy', 'azure_bluet', 'oxeye_daisy'] for n in range(lo, hi + 1): x, z = (n, fixed) if axis == 'x' else (fixed, n) self.put(x, y, z, 'minecraft:dirt') self.put(x, y + 1, z, 'minecraft:' + flowers[(n - lo) % len(flowers)]) dx, dz = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}[outward] self.trap(x + dx, y, z + dz, outward) self.stairs(x, y - 1, z, {'north': 'south', 'south': 'north', 'west': 'east', 'east': 'west'}[outward], half='top') for n, direction in [(lo - 1, 'west' if axis == 'x' else 'north'), (hi + 1, 'east' if axis == 'x' else 'south')]: x, z = (n, fixed) if axis == 'x' else (fixed, n) self.trap(x, y, z, direction) def terrain(self): """Lay an irregular garden island, approach, and side paths.""" for x in range(self.size_x): for z in range(self.size_z): corner = min(x, self.size_x - 1 - x) + min(z, self.size_z - 1 - z) if corner < 2: continue self.put(x, 0, z, 'minecraft:dirt') self.put(x, 1, z, 'minecraft:grass_block') for z in range(0, 10): center = 18 + (1 if z > 4 else 0) for x in range(center - 3, center + 4): self.put(x, 1, z, self.rng.choice(['minecraft:andesite'] * 3 + ['minecraft:gravel'] * 3 + ['minecraft:cobblestone', 'minecraft:coarse_dirt', 'minecraft:stone_bricks'])) for x in range(4, 29): for z in [5, 25, 26]: if self.rng.random() < .8: self.put(x, 1, z, self.rng.choice(['minecraft:coarse_dirt', 'minecraft:gravel', 'minecraft:mossy_cobblestone'])) # Exterior approach rises one block to the stone floor. for x in range(18, 22): self.stairs(x, 2, 8, 'south', 'stone_brick') self.box((18, 2, 9, 21, 2, 10), 'minecraft:stone_bricks') def stone_base(self): """Build the recessed stone shell and its shuttered front window.""" self.texture((8, 2, 9, 24, 2, 22), STONE) for y in range(3, 9): for x in range(8, 25): for z in [9, 22]: self.put(x, y, z, self.rng.choice(BASE if y == 3 else STONE)) for z in range(10, 22): for x in [8, 24]: self.put(x, y, z, self.rng.choice(BASE if y == 3 else STONE)) for x in [8, 24]: for z in [9, 22]: self.box((x, 3, z, x, 8, z), 'minecraft:stone_bricks') for x, hinge in [(19, 'left'), (20, 'right')]: self.door(x, 3, 9, hinge=hinge) self.timber((18, 3, 8, 18, 5, 8)) self.timber((21, 3, 8, 21, 5, 8)) self.timber((18, 5, 8, 21, 5, 8), 'x') self.stairs(18, 6, 8, 'east') self.stairs(21, 6, 8, 'west') for x in [19, 20]: self.slab(x, 6, 8) self.box((12, 4, 9, 15, 6, 9), 'minecraft:glass') self.timber((11, 4, 8, 11, 6, 8)) self.timber((16, 4, 8, 16, 6, 8)) self.timber((11, 7, 8, 16, 7, 8), 'x') for x in [12, 15]: for y in [4, 5, 6]: self.trap(x, y, 8, 'north') self.planter('x', 8, 12, 15, 3, 'north') # A small barred workshop window and a rear storage window. self.box((24, 5, 17, 24, 6, 19), 'minecraft:glass') self.box((13, 5, 22, 16, 6, 22), 'minecraft:glass') self.timber((8, 8, 9, 24, 8, 9), 'x', True) self.timber((8, 8, 22, 24, 8, 22), 'x', True) def postwork(self): """Carry the overhang on substantial posts, beams, and knee braces.""" posts = [(5, 6), (27, 6), (5, 24), (27, 24), (12, 6), (23, 6), (12, 24), (23, 24), (5, 15), (27, 15)] for x, z in posts: self.put(x, 2, z, 'minecraft:stone_bricks') self.timber((x, 3, z, x, 8, z)) for dx, dz, face in [(1, 0, 'west'), (-1, 0, 'east'), (0, 1, 'north'), (0, -1, 'south')]: for distance, y in [(1, 7), (2, 8)]: nx, nz = x + dx * distance, z + dz * distance if 5 <= nx <= 27 and 6 <= nz <= 24: self.stairs(nx, y, nz, face, half='top') nx, nz = x + dx, z + dz if 5 <= nx <= 27 and 6 <= nz <= 24: self.put(nx, 8, nz, 'minecraft:spruce_planks') # Cut log collars make each capital read as a joinery joint. self.put(x, 7, z, 'minecraft:spruce_log', axis='y') for x in [5, 27]: for z in [6, 24]: ix = x + (1 if x == 5 else -1) iz = z + (1 if z == 6 else -1) self.timber((ix, 2, z, ix, 8, z)) self.timber((x, 2, iz, x, 8, iz)) for x, z in posts: for dx, dz, face in [(1, 0, 'west'), (-1, 0, 'east'), (0, 1, 'north'), (0, -1, 'south')]: nx, nz = x + 2 * dx, z + 2 * dz if 5 <= nx <= 27 and 6 <= nz <= 24 and not (8 <= nx <= 24 and 9 <= nz <= 22): self.stairs(nx, 7, nz, face, half='top') for z in [6, 15, 24]: self.timber((5, 8, z, 27, 8, z), 'x', True) for x in [5, 12, 19, 27]: self.timber((x, 8, 6, x, 8, 24), 'z', True) self.texture((5, 9, 6, 27, 9, 24), FLOOR) for z in [6, 24]: self.timber((5, 9, z, 27, 9, z), 'x') for x in [5, 27]: self.timber((x, 9, 6, x, 9, 24), 'z') # A sheltered bench and cooper's barrels beneath the portico. for x in range(13, 17): self.stairs(x, 2, 7, 'south') for x in [12, 17]: self.trap(x, 2, 7, 'west' if x == 12 else 'east') for y in [2, 3]: self.put(7, y, 11, 'minecraft:barrel', facing='up', open='false') self.put(7, 2, 12, 'minecraft:barrel', facing='north', open='false') for x, z in [(7, 7), (25, 7), (7, 23), (25, 23)]: self.put(x, 8, z, 'minecraft:spruce_planks') self.lantern(x, 7, z, True) def timber_floor(self): """Wrap the main living floor in plaster, exposed timber, and tall glazing.""" for z in [6, 24]: self.texture((5, 10, z, 27, 15, z), PLASTER) for x in [5, 12, 19, 27]: self.timber((x, 10, z, x, 15, z)) for y in [10, 15]: self.timber((5, y, z, 27, y, z), 'x', y == 15) for a, b in [(8, 10), (14, 16), (21, 23)]: self.box((a, 11, z, b, 14, z), 'minecraft:light_gray_stained_glass') out = z - 1 if z == 6 else z + 1 facing = 'north' if z == 6 else 'south' for x in [a - 1, b + 1]: self.timber((x, 11, z, x, 14, z)) for y in [11, 12, 13]: self.trap(x, y, out, facing) self.planter('x', out, a, b, 10, facing) for x in range(a - 1, b + 2): self.stairs(x, 15, out, 'south' if z == 6 else 'north', half='top') for x in [5, 27]: self.texture((x, 10, 7, x, 15, 23), PLASTER) for z in [6, 13, 20, 24]: self.timber((x, 10, z, x, 15, z)) for y in [10, 15]: self.timber((x, y, 6, x, y, 24), 'z', y == 15) for a, b in [(8, 10), (16, 18), (21, 22)]: self.box((x, 11, a, x, 14, b), 'minecraft:light_gray_stained_glass') out = x - 1 if x == 5 else x + 1 facing = 'west' if x == 5 else 'east' for z in [a - 1, b + 1]: self.timber((x, 11, z, x, 14, z)) self.planter('z', out, a, b, 10, facing) self.texture((5, 16, 6, 27, 16, 24), FLOOR) for x in [5, 12, 19, 27]: self.timber((x, 15, 7, x, 15, 23), 'z', True) for z in [6, 24]: self.timber((5, 16, z, 27, 16, z), 'x') for x in [5, 27]: self.timber((x, 16, 6, x, 16, 24), 'z') def roof(self): """Raise a steep, backed stair roof and richly framed end gables.""" for z in range(4, 27): top = 17 + min(z - 4, 26 - z) facing = 'south' if z <= 15 else 'north' for x in range(3, 30): verge = x in [3, 29] self.put(x, top - 1, z, 'minecraft:spruce_planks' if verge else self.rng.choice(ROOF_SOLID)) if z == 15: self.put(x, top, z, 'minecraft:dark_oak_log', axis='x') self.slab(x, top + 1, z, 'stone_brick') else: material = 'spruce' if verge else self.rng.choice(['stone_brick'] * 12 + ['cobblestone'] * 4 + ['mossy_stone_brick']) self.stairs(x, top, z, facing, material) if x in [4, 28] and z != 15: self.stairs(x, top, z, facing, 'stone_brick') for x in [5, 27]: for z in range(6, 25): ceiling = 16 + min(z - 4, 26 - z) for y in range(17, ceiling + 1): self.put(x, y, z, self.rng.choice(PLASTER)) # Diagonal wooden verge and a central mast. self.put(x, ceiling, z, 'minecraft:stripped_spruce_log', axis='z') self.timber((x, 17, 15, x, 26, 15)) for y, low, high in [(18, 7, 23), (22, 11, 19)]: self.timber((x, y, low, x, y, high), 'z') for z in [11, 19]: self.timber((x, 17, z, x, 21, z)) for z in [12, 13, 17, 18]: self.box((x, 19, z, x, 21, z), 'minecraft:light_gray_stained_glass') self.box((x, 23, 14, x, 24, 14), 'minecraft:glass') self.box((x, 23, 16, x, 24, 16), 'minecraft:glass') out = x - 1 if x == 5 else x + 1 for z in range(10, 21): self.slab(out, 18, z, 'spruce', 'top') for z in [11, 15, 19]: self.stairs(out, 17, z, 'east' if x == 5 else 'west', half='top') self.timber((out, 25, 15, out, 28, 15)) self.put(out, 28, 15, 'minecraft:spruce_log', axis='x') for center in [10, 22]: self.dormer(center) def dormer(self, center): """Project a glazed little gable from the front roof into a bedroom.""" a, b = center - 2, center + 2 self.box((a + 1, 18, 8, b - 1, 23, 11), 'minecraft:air') for x in [a, b]: self.texture((x, 18, 7, x, 22, 11), PLASTER) self.timber((x, 18, 7, x, 22, 7)) self.texture((a, 18, 7, b, 22, 7), PLASTER) for x in [a, b]: self.timber((x, 18, 7, x, 22, 7)) self.timber((a, 18, 7, b, 18, 7), 'x') self.box((a + 1, 19, 7, b - 1, 21, 7), 'minecraft:glass') self.timber((a, 22, 7, b, 22, 7), 'x') self.put(center, 23, 7, 'minecraft:cut_sandstone') for x in range(a - 1, b + 2): top = 25 - abs(x - center) for z in range(6, 12): if top < 17 + min(z - 4, 26 - z): continue self.put(x, top - 1, z, self.rng.choice(ROOF_SOLID)) if x == center: self.slab(x, top, z, 'stone_brick') else: self.stairs(x, top, z, 'east' if x < center else 'west', 'stone_brick' if z != 6 else 'spruce') for x in range(a, b + 1): self.stairs(x, 17, 6, 'south', half='top') self.timber((center, 24, 6, center, 25, 6)) def cupola(self): """Crown the ridge with a compact glazed lookout and pointed finial.""" self.box((15, 27, 13, 19, 31, 17), 'minecraft:air') self.texture((14, 27, 12, 20, 27, 18), FLOOR) for x in range(15, 20): for z in range(13, 18): if x in [15, 19] or z in [13, 17]: self.timber((x, 25, z, x, 26, z)) for x in range(15, 20): for z in range(13, 18): if x in [15, 19] or z in [13, 17]: for y in range(28, 31): self.put(x, y, z, 'minecraft:glass') self.put(x, 31, z, 'minecraft:dark_oak_log', axis='x' if z in [13, 17] else 'z') for x in [15, 17, 19]: for z in [13, 17]: self.timber((x, 28, z, x, 31, z)) for x in [15, 19]: self.timber((x, 28, 15, x, 31, 15)) for y, radius in [(32, 3), (33, 2), (34, 2), (35, 1), (36, 0)]: for x in range(17 - radius, 18 + radius): for z in range(15 - radius, 16 + radius): edge = x in [17 - radius, 17 + radius] or z in [15 - radius, 15 + radius] self.put(x, y, z, self.rng.choice(ROOF_SOLID)) if edge and radius and y != 33: face = 'east' if x == 17 - radius else 'west' if x == 17 + radius else 'south' if z == 15 - radius else 'north' self.stairs(x, y, z, face, 'stone_brick') self.put(17, 37, 15, 'minecraft:cobblestone_wall', up='true', north='none', south='none', east='none', west='none', waterlogged='false') self.put(17, 38, 15, 'minecraft:iron_bars', north='false', south='false', east='false', west='false', waterlogged='false') for x in [14, 20]: for z in [13, 17]: self.stairs(x, 26, z, 'east' if x == 14 else 'west', half='top') def chimney(self): """Run a masonry flue from the living hearth to twin clay chimney pots.""" self.texture((23, 10, 12, 25, 30, 13), ['minecraft:stone_bricks'] * 4 + ['minecraft:cobblestone', 'minecraft:andesite']) for x in [23, 24, 25]: for z in [12, 13]: self.slab(x, 31, z, 'stone_brick', 'top') for x, z in [(23, 12), (25, 12)]: self.put(x, 32, z, 'minecraft:brick_wall', up='true', north='none', south='none', east='none', west='none', waterlogged='false') self.put(x, 33, z, 'minecraft:flower_pot') # The west-facing firebox is recessed behind the stone hearth lip. for z in [12, 13]: self.put(22, 10, z, 'minecraft:campfire', facing='west', lit='true', signal_fire='false', waterlogged='false') for z in [11, 14]: self.box((22, 10, z, 25, 12, z), 'minecraft:stone_bricks') self.box((22, 13, 11, 25, 13, 14), 'minecraft:dark_oak_planks') self.lantern(22, 14, 11) def furnishings(self): """Equip the workshop, great room, two bedchambers, and lookout.""" # Ground storage. A clear central aisle joins both staircases. for x in [13, 14, 17, 18, 21]: self.put(x, 3, 21, 'minecraft:barrel', facing='north', open='false') self.put(x, 4, 21, 'minecraft:barrel', facing='north', open='false') for x in [14, 17, 20]: self.put(x, 3, 11, 'minecraft:chest', facing='south', type='single', waterlogged='false') for x, block in [(20, 'crafting_table'), (21, 'smithing_table'), (22, 'furnace')]: self.put(x, 3, 20, 'minecraft:' + block, **({'facing': 'north', 'lit': 'true'} if block == 'furnace' else {})) self.put(22, 3, 18, 'minecraft:anvil', facing='north') self.put(23, 3, 15, 'minecraft:stonecutter', facing='west') self.put(23, 3, 14, 'minecraft:crafting_table') for x, z in [(10, 11), (17, 17), (21, 16), (15, 20), (19, 11)]: self.put(x, 8, z, 'minecraft:dark_oak_planks') self.lantern(x, 7, z, True) self.put(21, 4, 20, 'minecraft:potted_fern') # Dining table, chairs, patterned carpet, and fireside settle. for x in range(13, 19): for z in range(17, 21): self.put(x, 10, z, 'minecraft:' + ('red_carpet' if x in [13, 18] or z in [17, 20] else 'orange_carpet')) for x in range(14, 19): if x in [14, 18]: self.stairs(x, 10, 13, 'east' if x == 14 else 'west', 'dark_oak', 'top') else: self.slab(x, 10, 13, 'dark_oak', 'top') if x in [14, 16, 18]: self.stairs(x, 10, 11, 'north') self.stairs(x, 10, 15, 'south') self.candle(15, 11, 13, 3) self.put(17, 11, 13, 'minecraft:potted_red_tulip') for x in range(14, 19): self.stairs(x, 10, 22, 'south') for x in [13, 19]: self.put(x, 10, 22, 'minecraft:dark_oak_planks') self.candle(x, 11, 22, 2) self.box((20, 10, 23, 24, 10, 23), 'minecraft:bookshelf') self.box((19, 10, 21, 19, 12, 22), 'minecraft:bookshelf') for x in [9, 10, 11]: self.put(x, 10, 23, 'minecraft:barrel', facing='north', open='false') self.put(9, 11, 23, 'minecraft:potted_fern') self.lantern(11, 11, 23) for x, z in [(9, 9), (17, 9), (21, 19), (11, 20)]: self.put(x, 15, z, 'minecraft:dark_oak_planks') self.put(x, 14, z, 'minecraft:chain', axis='y', waterlogged='false') self.lantern(x, 13, z, True) # A roof-height partition gives the attic two actual bedrooms. for z in range(6, 25): height = min(25, 16 + min(z - 4, 26 - z)) self.texture((16, 17, z, 16, height, z), PLASTER) if z % 4 == 2: self.timber((16, 17, z, 16, height, z)) for z, hinge in [(14, 'left'), (15, 'right')]: self.door(16, 17, z, 'east', hinge) self.bed(10, 17, 20, 'north', 'red') self.bed(12, 17, 20, 'north', 'red') self.bed(22, 17, 10, 'south', 'blue') self.bed(24, 17, 10, 'south', 'blue') for x, z in [(11, 19), (23, 11), (13, 11), (20, 21)]: self.put(x, 17, z, 'minecraft:barrel', facing='up', open='false') self.lantern(x, 18, z) self.box((6, 17, 14, 6, 19, 16), 'minecraft:bookshelf') self.box((26, 17, 14, 26, 19, 16), 'minecraft:bookshelf') for x, z in [(10, 10), (20, 20)]: self.put(x, 17, z, 'minecraft:chest', facing='south', type='single', waterlogged='false') for x0, z0, color in [(10, 13, 'red'), (20, 15, 'blue')]: self.box((x0, 17, z0, x0 + 3, 17, z0 + 2), 'minecraft:' + color + '_carpet') self.put(26, 17, 20, 'minecraft:barrel', facing='west', open='false') self.candle(26, 18, 20) self.put(7, 17, 10, 'minecraft:barrel', facing='up', open='false') self.candle(7, 18, 10) # Lookout writing shelf and seating leave the ladder landing free. self.put(16, 28, 16, 'minecraft:bookshelf') self.candle(16, 29, 16, 4) self.stairs(16, 28, 14, 'west') self.put(18, 31, 14, 'minecraft:spruce_planks') self.lantern(18, 30, 14, True) def circulation(self): """Cut generous two-wide stairs and a continuous supported lookout ladder.""" for x0, z0, bottom, top in [(10, 13, 2, 9), (7, 12, 9, 16)]: for index in range(top - bottom): z, y = z0 + index, bottom + index + 1 for x in [x0, x0 + 1]: self.box((x, bottom + 1, z, x, y, z), 'minecraft:spruce_planks') self.box((x, y + 1, z, x, y + 3, z), 'minecraft:air') self.stairs(x, y, z, 'south') for x in [x0, x0 + 1]: self.box((x, top + 1, z0 + top - bottom, x, top + 2, z0 + top - bottom), 'minecraft:air') # Guard the long open edges of both stairwells. for x, y, lo, hi in [(9, 10, 17, 19), (12, 10, 17, 19), (9, 17, 16, 18)]: for z in range(lo, hi + 1): self.put(x, y, z, 'minecraft:spruce_fence', north=str(z > lo).lower(), south=str(z < hi).lower(), east='false', west='false', waterlogged='false') self.timber((18, 17, 17, 18, 29, 17)) for y in range(17, 30): self.put(18, y, 16, 'minecraft:ladder', facing='north', waterlogged='false') # A landing next to the top rung is open and two blocks tall. self.box((17, 28, 16, 17, 30, 16), 'minecraft:air') def spruce_tree(self, x, z, height, radius): """Grow a narrow, tiered spruce with persistent, connected foliage.""" self.box((x, 2, z, x, height, z), 'minecraft:spruce_log', axis='y') for y in range(5, height + 2, 3): r = max(1, min(radius, (height + 2 - y) // 3)) for dx in range(-r, r + 1): for dz in range(-r, r + 1): if abs(dx) + abs(dz) > r + 1: continue px, pz = x + dx, z + dz if self.inside(px, y, pz) and not (dx == 0 and dz == 0) and self.get(px, y, pz) in [None, 'minecraft:air']: self.put(px, y, pz, 'minecraft:spruce_leaves', persistent='true', distance=str(min(7, abs(dx) + abs(dz)))) for dx, dz in [(0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)]: if self.inside(x + dx, y + 1, z + dz) and self.get(x + dx, y + 1, z + dz) in [None, 'minecraft:air']: self.put(x + dx, y + 1, z + dz, 'minecraft:spruce_leaves', persistent='true', distance='1') self.put(x, height + 1, z, 'minecraft:spruce_leaves', persistent='true', distance='1') self.put(x, height + 2, z, 'minecraft:spruce_leaves', persistent='true', distance='2') def garden(self): """Finish the base with side spruces, ferns, shrubs, and flowers.""" for x, z, height, radius in [(2, 16, 23, 2), (30, 22, 18, 2), (2, 26, 13, 2), (30, 6, 11, 2)]: self.spruce_tree(x, z, height, radius) for x in range(1, 32): for z in range(2, 30): if self.get(x, 1, z) != 'minecraft:grass_block' or self.get(x, 2, z) is not None: continue if 7 <= x <= 25 and 8 <= z <= 23: continue roll = self.rng.random() if roll < .16: self.put(x, 2, z, 'minecraft:' + self.rng.choice(['short_grass'] * 4 + ['fern'] * 3 + ['dandelion', 'poppy'])) elif roll < .18 and (x < 4 or x > 28): self.put(x, 2, z, 'minecraft:azalea') for x, z in [(7, 4), (10, 4), (25, 27), (9, 26)]: self.put(x, 2, z, 'minecraft:flowering_azalea') def build(self) -> "Build": """Build the manor in structural order, then furnish and landscape it.""" self.terrain() self.stone_base() self.postwork() self.timber_floor() self.roof() self.cupola() self.chimney() self.furnishings() self.circulation() self.garden() return self def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) schematic.save(path) return path def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") if __name__ == "__main__": main()
stilted-manor/render.mjs — 74 行
// Render five exterior views of the finished stilted manor. // Lighting, camera framing and background are already tuned; run it as is to get a first look. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 768); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [226, 224, 210, 255]; const VIEWS = [ { name: 'hero', azimuth: 150, elevation: 13, radius: 3.3 }, { name: 'front', azimuth: 210, elevation: 16, radius: 3.25 }, { name: 'rear', azimuth: 35, elevation: 20, radius: 3.25 }, { 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})`); }
merchant-manor/generate.py — 850 行
# Starting point for a Minecraft build generator. Copy into the workspace and fill in build(). # Everything that has to be right for the harness is already right here: bounds checked placement, # one seeded random source, and a byte reproducible export. from __future__ import annotations import argparse import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Compound, String, Int, Byte, Float, Double, IntArray, List as NBTList from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (37, 43, 33), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Merchant Manor", "author": "generator", "description": "The Rowan Exchange: timber merchant manor, furnished hall, cooperage, market, bedrooms, library and lookout.", } STONE = ["cobblestone"] * 4 + ["stone_bricks"] * 5 + ["andesite"] * 2 + ["mossy_cobblestone"] PLASTER = ["smooth_sandstone"] * 9 + ["sandstone"] * 2 + ["cut_sandstone"] ROOF = ["cobbled_deepslate"] * 5 + ["deepslate_bricks"] * 4 + ["deepslate_tiles"] * 2 Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) return self.canvas.material(name, **dict(sorted(properties.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def p(self, x, y, z, name, **properties): """Place a vanilla block using a short name. Args: x, y, z (int): Block coordinates. name (str): Vanilla block name. **properties (str): State properties. """ self.put(x, y, z, 'minecraft:' + name, **properties) def fill(self, bounds, name, **properties): """Fill a box with a short vanilla name. Args: bounds (tuple): Inclusive XYZ bounds. name (str): Vanilla block name. **properties (str): State properties. """ self.box(bounds, 'minecraft:' + name, **properties) def mix(self, bounds, palette): """Texture a box with short vanilla names. Args: bounds (tuple): Inclusive XYZ bounds. palette (list): Weighted names. """ self.texture(bounds, ['minecraft:' + name for name in palette]) def door(self, x, y, z, facing='north', hinge='left'): """Place both matching halves of a door. Args: x, y, z (int): Lower block coordinates. facing, hinge (str): Door state. """ for dy, half in enumerate(('lower', 'upper')): self.p(x, y + dy, z, 'spruce_door', facing=facing, hinge=hinge, half=half, open='false', powered='false') def window(self, x, y, z, width=2, height=3, side='north'): """Fit full glass, solid sills, and exterior timber hoods. Args: x, y, z (int): First window block. width, height (int): Window dimensions. side (str): Outside face. """ along_x = side in ('north', 'south') dx, dz = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}[side] for i in range(width): a, b = x + (i if along_x else 0), z + (0 if along_x else i) for h in range(height): self.p(a, y + h, b, 'light_gray_stained_glass') self.windows.append((a, y + h, b, dx, dz)) self.p(a, y - 1, b, 'spruce_planks') self.p(a + dx, y - 1, b + dz, 'spruce_slab', type='top') self.p(a + dx, y + height, b + dz, 'spruce_stairs', facing=side, half='top', shape='straight') def light(self, x, y, z): """Put a lantern on a solid pedestal or surface. Args: x, y, z (int): Lantern coordinate. """ self.p(x, y, z, 'lantern', hanging='false', waterlogged='false') def chest(self, x, y, z, items, facing='north', barrel=False): """Place a stocked single chest or barrel with Java 1.21 item stacks. Args: x, y, z (int): Container coordinate. items (list): Item names and counts. facing (str): Front face. barrel (bool): Use a barrel. """ name = 'barrel' if barrel else 'chest' props = {'facing': facing, 'open': 'false'} if barrel else {'facing': facing, 'type': 'single', 'waterlogged': 'false'} self.p(x, y, z, name, **props) inventory = NBTList[Compound]([Compound({'Slot': Byte(i), 'id': String('minecraft:' + item), 'count': Int(count)}) for i, (item, count) in enumerate(items)]) self.tiles.append(Compound({'id': String('minecraft:' + name), 'x': Int(x), 'y': Int(y), 'z': Int(z), 'Items': inventory})) def frame(self, x, y, z, item, facing='north'): """Attach a visible item frame to the given solid block. Args: x, y, z (int): Supporting block. item (str): Display item. facing (str): Outward direction. """ dx, dz, number = {'north': (0, -1, 2), 'south': (0, 1, 3), 'west': (-1, 0, 4), 'east': (1, 0, 5)}[facing] tx, tz = x + dx, z + dz uid = [0x4D414E4F, 0x52000000, 0, len(self.entities) + 1] self.entities.append(Compound({'id': String('minecraft:item_frame'), 'Facing': Byte(number), 'TileX': Int(tx), 'TileY': Int(y), 'TileZ': Int(tz), 'Pos': NBTList[Double]([Double(x + .5 + dx * .53125), Double(y + .5), Double(z + .5 + dz * .53125)]), 'Rotation': NBTList[Float]([Float({'north': 180, 'south': 0, 'west': 90, 'east': 270}[facing]), Float(0)]), 'Motion': NBTList[Double]([Double(0), Double(0), Double(0)]), 'UUID': IntArray(uid), 'Invulnerable': Byte(0), 'Fixed': Byte(0), 'Invisible': Byte(0), 'ItemRotation': Byte(0), 'ItemDropChance': Float(1), 'Item': Compound({'id': String('minecraft:' + item), 'count': Int(1)})})) def bed(self, x, y, z, color='red', facing='north'): """Place a supported bed pair. Args: x, y, z (int): Foot coordinate. color, facing (str): Bed appearance and head direction. """ dx, dz = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}[facing] self.p(x, y, z, color + '_bed', facing=facing, part='foot', occupied='false') self.p(x + dx, y, z + dz, color + '_bed', facing=facing, part='head', occupied='false') def terrain(self): """Build a compact garden, worn paths, and a stone footing.""" self.fill((1, 0, 1, 35, 0, 31), 'grass_block') # Broad working yard in front, service walk around the manor. for x in range(2, 35): for z in range(2, 31): if z < 12 or x in (2, 3, 33, 34) or z >= 29: if self.rng.random() < .66: self.p(x, 0, z, self.rng.choice(['coarse_dirt'] * 4 + ['dirt_path'] * 3 + ['gravel', 'rooted_dirt'])) self.mix((5, 1, 11, 31, 1, 28), STONE) for x, z in [(3, 14), (3, 23), (34, 15), (34, 25), (7, 30), (25, 30), (12, 2), (22, 2)]: self.p(x, 1, z, 'mossy_cobblestone') self.p(x, 2, z, 'azalea_leaves', persistent='true', distance='1') if x in (3, 34): self.p(x, 3, z, 'oak_leaves', persistent='true', distance='1') for x in range(2, 35): for z in range(2, 31): if (x < 4 or x > 32 or z < 3 or z > 29) and self.get(x, 1, z) is None and self.get(x, 0, z) == 'minecraft:grass_block': if self.rng.random() < .45: self.p(x, 1, z, self.rng.choice(['short_grass'] * 6 + ['fern', 'oxeye_daisy', 'dandelion'])) def undercroft(self): """Raise a stone basement and the surrounding terrace.""" for bounds in [(5, 2, 11, 31, 7, 11), (5, 2, 28, 31, 7, 28), (5, 2, 12, 5, 7, 27), (31, 2, 12, 31, 7, 27)]: self.mix(bounds, STONE) self.fill((5, 7, 11, 31, 7, 11), 'stone_bricks') for x in (5, 14, 20, 23, 31): self.fill((x, 2, 10, x, 6, 11), 'stone_bricks') self.p(x, 6, 10, 'chiseled_stone_bricks') self.p(x, 7, 10, 'stone_brick_stairs', facing='south', half='top', shape='straight') for z in (16, 22, 28): for x in (4, 32): self.fill((x, 1, z, x, 6, z), 'stone_bricks') self.p(x, 6, z, 'chiseled_stone_bricks') self.p(x, 7, z, 'stone_brick_stairs', facing='east' if x == 4 else 'west', half='top') self.fill((4, 8, 10, 32, 8, 29), 'stone_bricks') self.mix((5, 8, 11, 31, 8, 28), ['spruce_planks'] * 8 + ['dark_oak_planks']) # Polished, shouldered arch and a glazed transom above the double door. for x in (9, 12): self.fill((x, 2, 10, x, 4, 11), 'polished_andesite') self.fill((9, 5, 10, 12, 5, 11), 'polished_andesite') self.p(9, 4, 10, 'polished_andesite_stairs', facing='east', half='top') self.p(12, 4, 10, 'polished_andesite_stairs', facing='west', half='top') for x in (10, 11): self.door(x, 2, 11, hinge='left' if x == 10 else 'right') self.p(x, 4, 11, 'light_gray_stained_glass') self.p(x, 1, 10, 'polished_andesite') # Glass behind iron bars closes the envelope while preserving the barred look. for x in (7, 17, 22): for y in (3, 4): self.p(x, y, 11, 'gray_stained_glass') if x != 17: self.p(x, y, 10, 'iron_bars', north='false', south='false', east='true', west='true') self.p(x, 2, 10, 'stone_brick_slab', type='top') for z in (15, 19, 25): for y in (3, 4): self.p(31, y, z, 'gray_stained_glass') self.p(32, y, z, 'iron_bars', north='true', south='true', east='false', west='false') self.p(32, 2, z, 'stone_brick_slab', type='top') for x in (10, 17, 25): self.window(x, 3, 28, 2, 2, 'south') # Market stock room partition, one doorway, no inaccessible compartments. self.fill((21, 2, 12, 21, 7, 27), 'stone_bricks') self.door(21, 2, 20, facing='east') for x in (26, 27): self.door(x, 2, 11, hinge='left' if x == 26 else 'right') self.fill((25, 4, 10, 28, 4, 11), 'spruce_planks') # Eight broad steps make the terrace the obvious public entrance. for z in range(3, 11): y = z - 2 self.mix((15, 0, z, 19, y - 1, z), STONE) self.fill((15, y, z, 19, y, z), 'stone_brick_stairs', facing='south', half='bottom', shape='straight') for x in (14, 20): self.fill((x, 0, z, x, y, z), 'stripped_spruce_log', axis='y') self.p(x, y + 1, z, 'spruce_fence', north='true', south='true') for x in range(4, 33): if x not in range(14, 21): self.p(x, 9, 10, 'spruce_fence', east='true', west='true') self.p(x, 9, 29, 'spruce_fence', east='true', west='true') for z in range(11, 29): for x in (4, 32): self.p(x, 9, z, 'spruce_fence', north='true', south='true') for x, z in [(4, 10), (32, 10), (4, 29), (32, 29)]: self.p(x, 9, z, 'stripped_dark_oak_log') self.light(x, 10, z) def timber(self): """Frame the hall with warm plaster panels and deep window reveals.""" for bounds in [(7, 9, 13, 29, 14, 13), (7, 9, 26, 29, 14, 26), (7, 9, 14, 7, 14, 25), (29, 9, 14, 29, 14, 25)]: self.mix(bounds, PLASTER) self.fill((7, 15, 12, 29, 15, 26), 'spruce_planks') for z in (13, 26): for x in (7, 12, 15, 20, 24, 29): self.fill((x, 9, z, x, 14, z), 'stripped_spruce_log', axis='y') for y in (9, 14): self.fill((7, y, z, 29, y, z), 'dark_oak_log', axis='x') for x in (7, 29): for z in (13, 18, 22, 26): self.fill((x, 9, z, x, 14, z), 'stripped_spruce_log', axis='y') self.fill((x, 14, 13, x, 14, 26), 'dark_oak_log', axis='z') for x in (9, 22, 26): self.window(x, 10, 13) for x in (9, 17, 25): self.window(x, 10, 26, side='south') for x, side in ((7, 'west'), (29, 'east')): for z in (15, 20, 24): self.window(x, 10, z, 1, 3, side) for x in (16, 19): self.fill((x, 9, 12, x, 12, 13), 'stripped_dark_oak_log') self.door(17, 9, 13) self.door(18, 9, 13, hinge='right') self.fill((16, 12, 12, 19, 12, 13), 'spruce_planks') self.fill((16, 12, 11, 19, 12, 11), 'spruce_stairs', facing='south', half='bottom') for x in (15, 20): self.p(x, 10, 12, 'spruce_planks') self.light(x, 11, 12) for z in (12, 27): self.fill((6, 15, z, 30, 15, z), 'spruce_slab', type='top') # Repeating carved corbels make the terrace facade read as heavy timber. for x in (7, 12, 15, 20, 24, 29): self.p(x, 13, 12, 'spruce_stairs', facing='south', half='top') self.p(x, 14, 12, 'spruce_planks') self.p(x, 13, 27, 'spruce_stairs', facing='north', half='top') def roofs(self): """Join two steep roofs with a closed union of their roof surfaces.""" heights = {} for x in range(6, 31): for z in range(11, 29): h = 16 + min(z - 11, 28 - z) heights[x, z] = (h, 'south' if z < 20 else 'north', x in (6, 30) or z in (11, 28)) for x in range(21, 32): for z in range(10, 21): h = 16 + (min(x - 21, 31 - x) * 3) // 2 if h >= heights.get((x, z), (-1,))[0]: heights[x, z] = (h, 'east' if x < 26 else 'west', z == 10 or x in (21, 31)) # Close the upper storey before laying the weather surface. for x in range(7, 30): for z in range(13, 27): if x in (7, 29) or z in (13, 26): h = heights[x, z][0] self.mix((x, 16, z, x, h - 1, z), PLASTER) if x in (7, 12, 15, 20, 24, 29) or z in (18, 22): self.fill((x, 16, z, x, h - 1, z), 'stripped_spruce_log') # Front cross gable projects one block over the hall wall. for x in range(22, 31): h = heights[x, 12][0] self.mix((x, 15, 12, x, h - 1, 12), PLASTER) if x in (22, 26, 30): self.fill((x, 16, 12, x, h - 1, 12), 'stripped_spruce_log') for z in (11, 12): for x in (22, 30): self.fill((x, 15, z, x, heights[x, z][0] - 1, z), 'spruce_planks') for (x, z), (h, direction, trim) in heights.items(): self.p(x, h, z, 'spruce_planks' if trim else self.rng.choice(ROOF)) self.p(x, h + 1, z, 'spruce_stairs' if trim else 'cobbled_deepslate_stairs', facing=direction, half='bottom', shape='straight') # Tall cross gable steps have a filled riser beneath the next course. for nx, nz in ((x - 1, z), (x + 1, z), (x, z - 1), (x, z + 1)): if (nx, nz) in heights and heights[nx, nz][0] < h - 1: self.p(x, h - 1, z, 'spruce_planks' if trim else self.rng.choice(ROOF)) # Carved end grain bosses, raised eaves and ridge finials. for x in (6, 30): for z in (11, 13, 15, 17, 20, 22, 24, 26, 28): h = heights[x, z][0] self.p(x, h, z, 'spruce_log', axis='x') self.p(x, h - 1, z, 'spruce_stairs', facing='east' if x == 6 else 'west', half='top') for x in (21, 23, 25, 27, 29, 31): h = heights[x, 10][0] self.p(x, h, 10, 'spruce_log', axis='z') self.p(x, h - 1, 10, 'spruce_stairs', facing='south', half='top') self.window(25, 18, 12, 2, 3) self.window(7, 18, 17, 1, 3, 'west') self.window(29, 18, 21, 2, 3, 'east') for x in (6, 30): for z in (11, 28): self.p(x, 18, z, 'spruce_slab', type='bottom') self.roof_heights = heights def tower(self): """Add the central study tower, gabled crown, and smaller western spire.""" # The tower cuts through the main roof; its walls replace the roof intersections. self.fill((15, 16, 17, 23, 32, 25), 'air') for bounds in [(15, 16, 17, 23, 32, 17), (15, 16, 25, 23, 32, 25), (15, 16, 18, 15, 32, 24), (23, 16, 18, 23, 32, 24)]: self.mix(bounds, PLASTER) for x in (15, 19, 23): for z in (17, 25): self.fill((x, 16, z, x, 32, z), 'stripped_spruce_log') for x in (15, 23): for z in (21,): self.fill((x, 16, z, x, 32, z), 'stripped_spruce_log') for y in (22, 28, 32): for z in (17, 25): self.fill((15, y, z, 23, y, z), 'dark_oak_log', axis='x') for x in (15, 23): self.fill((x, y, 17, x, y, 25), 'dark_oak_log', axis='z') for y in (22, 28): self.fill((16, y, 18, 22, y, 24), 'spruce_planks') self.door(15, 16, 18, facing='west') self.door(23, 16, 20, facing='east') for y, height in ((24, 3), (29, 3)): for x in (17, 21): self.window(x, y, 17, 1, height) self.window(x, y, 25, 1, height, 'south') for x, side in ((15, 'west'), (23, 'east')): self.window(x, 26 if y == 24 else y, 19, 1, 2 if y == 24 else height, side) self.window(x, y, 23, 1, height, side) for x in range(14, 25): h = 33 + (min(x - 14, 24 - x) * 6) // 5 for z in (17, 25): self.mix((x, 33, z, x, max(33, h - 1), z), PLASTER) if x == 19: self.fill((x, 33, z, x, h - 1, z), 'stripped_spruce_log') for z in range(16, 27): trim = z in (16, 26) or x in (14, 24) self.p(x, h, z, 'spruce_planks' if trim else self.rng.choice(ROOF)) self.p(x, h + 1, z, 'spruce_stairs' if trim else 'deepslate_tile_stairs', facing='east' if x < 19 else 'west', half='bottom') if x == 19: self.p(x, h + 1, z, 'spruce_slab', type='bottom') if min(x - 14, 24 - x) == 5: self.p(x, h - 1, z, 'spruce_planks' if trim else 'deepslate_bricks') if x % 2 == 0: for z in (16, 26): self.p(x, h, z, 'spruce_log', axis='z') self.p(x, h - 1, z, 'spruce_stairs', facing='south' if z == 16 else 'north', half='top') for z in (16, 26): self.p(19, 40, z, 'spruce_planks') self.p(19, 41, z, 'spruce_fence') self.window(19, 35, 17, 1, 2) self.window(19, 35, 25, 1, 2, 'south') # Ladder stays in one corner, leaving each landing walkable. for y in range(16, 30): self.p(22, y, 24, 'ladder', facing='west', waterlogged='false') # Secondary turret: stone shaft, small accessible watch room, steep slate cap. self.fill((7, 16, 19, 11, 28, 23), 'air') for bounds in [(7, 16, 19, 11, 28, 19), (7, 16, 23, 11, 28, 23), (7, 16, 20, 7, 28, 22), (11, 16, 20, 11, 28, 22)]: self.mix(bounds, ['andesite', 'polished_andesite', 'diorite', 'andesite']) self.fill((8, 23, 20, 10, 23, 22), 'spruce_planks') self.door(11, 16, 21, facing='east') for x, y, z, side in [(9, 25, 19, 'north'), (7, 25, 21, 'west'), (11, 25, 21, 'east'), (9, 25, 23, 'south')]: self.window(x, y, z, 1, 2, side) for y in range(16, 25): self.p(10, y, 22, 'ladder', facing='west') for y, radius in [(29, 3), (30, 2), (31, 2), (32, 1), (33, 1), (34, 0)]: for x in range(9 - radius, 10 + radius): for z in range(21 - radius, 22 + radius): name = 'spruce_planks' if y == 29 else self.rng.choice(ROOF) self.p(x, y, z, name) if y == 29 and (x in (6, 12) or z in (18, 24)): self.p(x, y + 1, z, 'spruce_slab', type='bottom') self.p(9, 35, 21, 'deepslate_brick_wall') self.p(9, 36, 21, 'lightning_rod', facing='up') def stairs_and_furnishings(self): """Connect every level and furnish the storage, hall, bedrooms and study.""" # Internal service stairs from storage to hall, with a three-block clear stairwell. for z in range(20, 27): y = 28 - z self.fill((18, y + 1, z, 19, y + 3, z), 'air') self.fill((18, 2, z, 19, y - 1, z), 'stone_bricks') if y > 2 else None self.fill((18, y, z, 19, y, z), 'stone_brick_stairs', facing='north', half='bottom') for z in range(20, 25): self.p(17, 9, z, 'spruce_fence', north='true', south='true') # Main stair to the attic, away from the hearth and entrance. for z in range(17, 24): y = 32 - z self.fill((9, y + 1, z, 10, y + 3, z), 'air') if y > 9: self.fill((9, 9, z, 10, y - 1, z), 'spruce_planks') self.fill((9, y, z, 10, y, z), 'spruce_stairs', facing='north', half='bottom') # A little upper landing also leads to the spire ladder. self.fill((8, 15, 18, 8, 15, 19), 'spruce_planks') self.fill((8, 16, 18, 8, 18, 18), 'air') self.door(11, 16, 21, facing='east') for y in range(16, 25): self.p(10, y, 22, 'ladder', facing='west') # Undercroft cooperage and goods racks. for x in (6, 7, 8, 13, 14): for y in (2, 3): self.chest(x, y, 27, [('wheat', 32), ('potato', 24)], barrel=True) for z in (15, 17, 23): self.chest(6, 2, z, [('iron_ingot', 12), ('coal', 32)]) self.p(6, 4, z, 'spruce_planks') self.light(6, 5, z) self.chest(13, 2, 14, [('emerald', 18), ('gold_ingot', 8)]) self.fill((12, 2, 22, 14, 2, 23), 'hay_block', axis='y') self.p(14, 3, 23, 'hay_block', axis='y') self.fill((11, 2, 18, 13, 2, 18), 'spruce_planks') self.p(11, 3, 18, 'crafting_table') self.light(13, 3, 18) self.frame(14, 3, 27, 'wheat') self.p(17, 2, 12, 'stone_bricks') self.light(17, 3, 12) self.p(20, 2, 17, 'stone_bricks') self.light(20, 3, 17) self.p(8, 16, 20, 'spruce_planks') self.light(8, 17, 20) self.p(16, 2, 27, 'stone_bricks') self.light(16, 3, 27) for x, z in ((23, 26), (25, 26), (29, 26), (29, 17), (29, 21)): self.chest(x, 2, z, [('bread', 16), ('apple', 24), ('carrot', 32)], barrel=True) for x, z in ((23, 14), (29, 14), (23, 24), (29, 24)): self.p(x, 2, z, 'spruce_planks') self.light(x, 3, z) self.fill((24, 2, 17, 27, 2, 17), 'spruce_planks') self.p(24, 3, 17, 'crafting_table') self.p(27, 3, 17, 'flower_pot') self.frame(25, 2, 26, 'apple') # Hall: carpet runner and merchant counter. for x in range(13, 17): for z in range(15, 20): self.p(x, 9, z, 'red_carpet' if x in (13, 16) or z in (15, 19) else 'orange_carpet') self.fill((21, 9, 16, 25, 9, 16), 'dark_oak_planks') self.p(21, 10, 16, 'potted_fern') self.p(23, 10, 16, 'yellow_candle', candles='3', lit='true') self.light(25, 10, 16) self.p(23, 9, 18, 'spruce_stairs', facing='south') self.chest(25, 9, 18, [('emerald', 32), ('paper', 16)]) self.frame(24, 11, 13, 'emerald', facing='south') # Seating and dining table. self.fill((12, 9, 23, 15, 9, 23), 'dark_oak_planks') self.p(12, 10, 23, 'white_candle', candles='3', lit='true') self.p(15, 10, 23, 'flower_pot') for x in (12, 14): self.p(x, 9, 22, 'spruce_stairs', facing='north') if x == 12: self.p(x, 9, 24, 'spruce_stairs', facing='south') # Hearth and stone flue sit against the east wall, clear of windows. self.fill((26, 9, 22, 28, 9, 24), 'stone_bricks') self.fill((28, 10, 22, 28, 13, 24), 'bricks') self.p(27, 10, 23, 'campfire', facing='west', lit='true', signal_fire='false') self.fill((26, 10, 22, 26, 11, 22), 'polished_andesite') self.fill((26, 10, 24, 26, 11, 24), 'polished_andesite') self.fill((26, 12, 22, 28, 12, 24), 'stone_bricks') self.p(26, 10, 23, 'iron_bars', north='true', south='true') self.fill((27, 13, 22, 28, 14, 24), 'bricks') # Kitchen keeps a clear central passage to the service stair. self.fill((21, 9, 25, 24, 9, 25), 'spruce_planks') self.p(21, 9, 25, 'smoker', facing='north', lit='false') self.p(22, 9, 25, 'furnace', facing='north', lit='false') self.p(23, 9, 25, 'water_cauldron', level='3') self.p(24, 9, 25, 'crafting_table') self.light(24, 10, 25) self.fill((21, 12, 25, 25, 12, 25), 'spruce_planks') self.p(22, 13, 25, 'flower_pot') for x, z in ((8, 14), (13, 25), (20, 14)): self.p(x, 9, z, 'spruce_planks') self.light(x, 10, z) # Attic bedrooms, with trunks, carpets and bedside candles. self.bed(12, 16, 16, 'red') self.bed(13, 16, 16, 'red') self.chest(13, 16, 18, [('leather_boots', 1), ('bread', 4)]) self.p(11, 16, 15, 'spruce_planks') self.light(11, 17, 15) self.p(12, 16, 17, 'red_carpet') self.bed(26, 16, 16, 'blue') self.p(27, 16, 15, 'spruce_planks') self.light(27, 17, 15) self.chest(28, 16, 18, [('book', 5), ('compass', 1)]) for z in range(18, 24): self.p(25, 16, z, 'blue_carpet') self.fill((27, 16, 24, 28, 17, 24), 'bookshelf') self.p(26, 16, 24, 'spruce_planks') self.light(26, 17, 24) # Tower lower landing: quiet map room below the study. self.fill((16, 16, 19, 16, 18, 22), 'bookshelf') self.p(17, 16, 20, 'spruce_planks') self.light(17, 17, 20) self.p(20, 16, 19, 'cartography_table') self.frame(19, 18, 17, 'map', facing='south') # Study, accessible directly from the ladder landing. self.fill((16, 23, 20, 16, 25, 23), 'bookshelf') self.fill((18, 23, 18, 20, 23, 18), 'dark_oak_planks') self.p(18, 24, 18, 'potted_dead_bush') self.p(20, 24, 18, 'white_candle', candles='3', lit='true') self.p(19, 23, 20, 'spruce_stairs', facing='south') self.p(21, 23, 21, 'lectern', facing='west', has_book='true', powered='false') self.tiles.append(Compound({'id': String('minecraft:lectern'), 'x': Int(21), 'y': Int(23), 'z': Int(21), 'Page': Int(0), 'Book': Compound({'id': String('minecraft:written_book'), 'count': Int(1), 'components': Compound({'minecraft:written_book_content': Compound({'title': String('Merchant Ledger'), 'author': String('House of the Rowan'), 'generation': Int(0), 'resolved': Byte(1), 'pages': NBTList[String]([String('{"text":"Autumn ledger\\n\\nWheat, wool, copper and salt.\\n\\nA warm hearth for every traveler."}')])})})})})) self.p(16, 23, 24, 'spruce_planks') self.light(16, 24, 24) self.chest(20, 23, 24, [('emerald', 12), ('book', 6), ('compass', 1)]) self.fill((18, 23, 21, 19, 23, 23), 'green_carpet') # Top lookout: a bench, telescope stand and map cabinet. self.fill((16, 29, 21, 16, 29, 23), 'spruce_stairs', facing='west') self.p(19, 29, 18, 'cartography_table') self.p(19, 30, 18, 'yellow_candle', candles='2', lit='true') self.p(21, 29, 21, 'spruce_planks') self.frame(21, 29, 21, 'spyglass') self.p(16, 29, 24, 'spruce_planks') self.light(16, 30, 24) self.fill((18, 29, 21, 19, 29, 23), 'green_carpet') # The tiny turret has a useful lookout and its own light. self.p(8, 24, 20, 'spruce_planks') self.light(8, 25, 20) self.p(8, 24, 22, 'spruce_stairs', facing='west') def market_and_chimney(self): """Build the striped street stall, left loading shelter and smoking brick stack.""" for x in (23, 31): self.fill((x, 1, 6, x, 5, 6), 'spruce_fence', north='false', south='false', east='false', west='false') self.p(x, 1, 6, 'stripped_spruce_log') for x in range(23, 32): color = 'red_wool' if (x - 23) // 2 % 2 == 0 else 'white_wool' for z in range(6, 11): y = 5 if z <= 7 else 6 self.p(x, y, z, color) self.p(x, 4, 6, color) self.fill((23, 6, 10, 31, 6, 10), 'spruce_planks') self.fill((24, 1, 7, 30, 1, 7), 'spruce_planks') self.fill((24, 2, 7, 30, 2, 7), 'barrel', facing='up', open='false') for x, block in ((24, 'pumpkin'), (25, 'melon'), (29, 'hay_block')): self.p(x, 3, 7, block) self.p(27, 3, 7, 'flower_pot') self.light(30, 3, 7) self.frame(26, 2, 7, 'bread') self.frame(28, 2, 7, 'apple') # A gabled trade sign, solidly attached to the stall beam. self.fill((25, 7, 10, 29, 7, 10), 'dark_oak_planks') self.frame(27, 7, 10, 'emerald') # Left lean-to, loaded with coopered casks and bundled bales. for x in (2, 8): for z in (5, 10): self.fill((x, 1, z, x, 5, z), 'stripped_spruce_log') self.p(x, 1, z, 'stone_bricks') self.fill((2, 1, 5, 8, 1, 10), 'spruce_planks') for z in range(4, 12): y = 5 + (z - 4) // 3 self.fill((1, y, z, 9, y, z), 'spruce_planks') self.fill((1, y + 1, z, 9, y + 1, z), 'spruce_slab', type='bottom') for x, z in ((3, 9), (4, 9), (7, 9), (7, 7)): self.chest(x, 2, z, [('wheat', 32), ('coal', 16)], barrel=True) self.chest(4, 2, 6, [('iron_ingot', 16), ('copper_ingot', 24)]) self.p(3, 3, 9, 'barrel', facing='north', open='false') self.fill((5, 2, 9, 6, 3, 9), 'hay_block', axis='x') self.light(7, 3, 7) # A tall, offset brick stack, with a real signal campfire at its open top. self.fill((27, 15, 23, 28, 31, 24), 'bricks') for y in (18, 25, 29): self.fill((27, y, 23, 28, y, 24), 'granite') for y in (26, 32): self.fill((26, y, 22, 29, y, 25), 'brick_slab', type='top') self.fill((27, y, 23, 28, y, 24), 'bricks') self.p(27, 32, 23, 'hay_block') self.p(27, 33, 23, 'campfire', facing='north', lit='true', signal_fire='true') self.p(28, 33, 24, 'brick_wall') self.p(28, 33, 23, 'brick_wall') self.p(27, 33, 24, 'brick_wall') def greenery(self): """Attach climbing vines and compact ivy clusters to existing masonry and timber.""" # Only place wall vines against solid, window-free facade blocks. for x, z, levels, face in [(8, 12, range(9, 14), 'south'), (13, 12, range(9, 15), 'south'), (30, 17, range(9, 15), 'west'), (6, 24, range(9, 14), 'east'), (14, 20, range(23, 32), 'east'), (16, 16, range(21, 30), 'south'), (22, 16, range(23, 31), 'south'), (24, 24, range(24, 32), 'west'), (6, 11, range(2, 7), 'south')]: dx, dz = {'south': (0, 1), 'north': (0, -1), 'east': (1, 0), 'west': (-1, 0)}[face] for y in levels: support = self.get(x + dx, y, z + dz) or 'air' if self.get(x, y, z) in (None, 'minecraft:air') and all(word not in support for word in ('air', 'glass', 'door', 'stairs', 'slab')): self.p(x, y, z, 'vine', **{face: 'true'}) # Roof ivy is physically rooted to the wood edge, then grows in connected leaves. clusters = [(6, 18, 11), (6, 21, 14), (14, 26, 17), (24, 30, 22), (15, 24, 16), (21, 23, 10), (5, 3, 13), (13, 2, 10)] protected = {(x + dx, y, z + dz) for x, y, z, dx, dz in self.windows} offsets = [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)] for cx, cy, cz in clusters: for _ in range(95): x = cx + self.rng.randint(-1, 1) y = cy + self.rng.randint(-3, 1) z = cz + self.rng.randint(-1, 1) if not self.inside(x, y, z) or (x, y, z) in protected or self.get(x, y, z) not in (None, 'minecraft:air'): continue adjacent = [self.get(x + dx, y + dy, z + dz) or 'air' for dx, dy, dz in offsets] if any(any(w in s for w in ('log', 'planks', 'deepslate', 'leaves', 'cobblestone')) for s in adjacent): self.p(x, y, z, 'oak_leaves', persistent='true', distance='1') # Planters stand on solid terrace decking, clear of the entrance. for x, z in ((6, 12), (30, 13), (6, 27), (30, 27)): self.p(x, 9, z, 'composter', level='8') self.p(x, 10, z, 'azalea_leaves', persistent='true', distance='1') def orient_front(self): """Reflect the plan so the north-facing facade matches the reference's left and right. The architectural plan uses eastward coordinates for the market wing. Seen from the street, west is on the viewer's right. Reflect geometry, states, block entities and hanging entities together before export. """ placed = self.placed self.placed = {} self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) swap = {'east': 'west', 'west': 'east'} for (x, y, z), state in placed.items(): name, _, raw = state.partition('[') props = dict(part.split('=') for part in raw.rstrip(']').split(',') if '=' in part) mirrored = {} for key, value in props.items(): if key == 'facing': value = swap.get(value, value) elif key == 'hinge': value = 'right' if value == 'left' else 'left' elif key == 'shape': value = {'inner_left': 'inner_right', 'inner_right': 'inner_left', 'outer_left': 'outer_right', 'outer_right': 'outer_left'}.get(value, value) mirrored[swap.get(key, key)] = value self.put(self.size_x - 1 - x, y, z, name, **mirrored) for tile in self.tiles: tile['x'] = Int(self.size_x - 1 - int(tile['x'])) for entity in self.entities: entity['TileX'] = Int(self.size_x - 1 - int(entity['TileX'])) entity['Pos'][0] = Double(self.size_x - float(entity['Pos'][0])) entity['Facing'] = Byte({4: 5, 5: 4}.get(int(entity['Facing']), int(entity['Facing']))) entity['Rotation'][0] = Float((-float(entity['Rotation'][0])) % 360) self.windows = [(self.size_x - 1 - x, y, z, -dx, dz) for x, y, z, dx, dz in self.windows] def build(self) -> 'Build': """Construct the complete manor from ground up. Returns: Build: This furnished instance. """ self.tiles, self.entities, self.windows = [], [], [] self.terrain() self.undercroft() self.timber() self.roofs() self.tower() self.stairs_and_furnishings() self.market_and_chimney() self.greenery() self.orient_front() return self def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) nbt = schematic.write_to_nbt() region = nbt["Regions"][CONFIG["name"]] region["TileEntities"] = NBTList[Compound](self.tiles) region["Entities"] = NBTList[Compound](self.entities) NBTFile(nbt).save(path, gzipped=False) return path def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": len(self.placed), "palette_states": len(loaded.palette), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") if __name__ == "__main__": main()
merchant-manor/render.mjs — 73 行
// Starting point for rendering a build. Copy into the workspace and edit VIEWS. // Lighting, camera framing and background are already tuned; run it as is to get a first look. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 900); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [47, 52, 59, 255]; const VIEWS = [ { name: 'front', azimuth: 210, elevation: 16, radius: 3.0 }, { name: 'rear', azimuth: 35, elevation: 22, radius: 3.0 }, { 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})`); }
mediterranean-villa/generate.py — 782 行
# Generate and verify a furnished Java 1.21.1 Mediterranean villa from a seeded block plan. from __future__ import annotations import argparse import random import sys from pathlib import Path from typing import Dict, Iterable, Optional, Tuple sys.path.insert(0, "/work/generator/MCIO") from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Compound, Int, Byte, Float, Double, String, IntArray, List as NBTList from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402 CONFIG = { "seed": 20260923, "size_xyz": (35, 27, 31), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Villa_al_Mare", "author": "generator", "description": "Furnished Mediterranean villa, two floors, two pergolas and a garden pool. Front faces south (+Z).", } PLASTER = ["white_concrete"] * 7 + ["smooth_quartz"] * 3 + ["calcite"] FLOOR = ["oak_planks"] * 10 + ["birch_planks"] + ["bamboo_planks"] * 2 DIRECTIONS = {"north": (0, -1), "south": (0, 1), "west": (-1, 0), "east": (1, 0)} Position = Tuple[int, int, int] class Build: """Hold a block volume and write it out as a Litematica schematic.""" def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None: """Create an empty volume with one seeded random source. Args: size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z. seed (int): Seed for every random decision in this build. """ self.size_x, self.size_y, self.size_z = size_xyz self.rng = random.Random(seed) self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x)) self.placed: Dict[Position, str] = {} self.entities = [] self.block_entities = [] def inside(self, x: int, y: int, z: int) -> bool: """Report whether a coordinate is inside the volume. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: bool: True when every axis is within range. """ return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None: """Place one block, in XYZ order, with an immediate bounds check. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. block (str | Material): Namespaced block name, a full blockstate string, or a Material. **properties: Blockstate properties such as facing or half. Raises: AssertionError: When the coordinate is outside the volume, naming the coordinate. """ assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}" material = self.material(block, **properties) if isinstance(block, str) else block self.canvas.block((y, z, x), material) self.placed[(x, y, z)] = material.blockstate def material(self, name: str, **properties: str) -> Material: """Build a Material from a block name and its properties. Args: name (str): Namespaced block name, or a full blockstate string. **properties: Blockstate properties. Returns: Material: Material ready for placement. """ if "[" in name: return parse_blockstate(name) return self.canvas.material(name, **dict(sorted(properties.items()))) def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill an inclusive box. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, block, **properties) def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None: """Fill only the faces of a box, leaving the inside empty. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. block (str | Material): Block to place. **properties: Blockstate properties. """ x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1) if on_face: self.put(x, y, z, block, **properties) def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None: """Fill a box with a weighted mix of related blocks, so the surface is not flat. Args: bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive. palette (Iterable[str]): Block names. Repeat a name to make it more common. """ choices = list(palette) x0, y0, z0, x1, y1, z1 = bounds for x in range(min(x0, x1), max(x0, x1) + 1): for y in range(min(y0, y1), max(y0, y1) + 1): for z in range(min(z0, z1), max(z0, z1) + 1): self.put(x, y, z, self.rng.choice(choices)) def get(self, x: int, y: int, z: int) -> Optional[str]: """Read back what was placed at a coordinate. Args: x (int): Width coordinate. y (int): Height coordinate. z (int): Depth coordinate. Returns: Optional[str]: Blockstate string, or None when the cell is empty. """ return self.placed.get((x, y, z)) def p(self, x: int, y: int, z: int, name: str, **properties: str) -> None: """Place a vanilla block using its short name. Args: x, y, z: Block coordinates. name: Vanilla block name. **properties: Blockstate properties. """ self.put(x, y, z, f"minecraft:{name}", **properties) def fill(self, bounds: tuple, name: str, **properties: str) -> None: """Fill a named architectural volume. Args: bounds: Inclusive XYZ bounds. name: Vanilla block name. **properties: Blockstate properties. """ self.box(bounds, f"minecraft:{name}", **properties) def mix(self, bounds: tuple, names: list) -> None: """Texture a surface with the seeded palette. Args: bounds: Inclusive XYZ bounds. names: Weighted vanilla material names. """ self.texture(bounds, [f"minecraft:{name}" for name in names]) def door(self, x: int, y: int, z: int, facing: str, hinge: str = "left", wood: str = "warped") -> None: """Install a complete closed door. Args: x, y, z: Lower-half coordinates. facing: Door direction. hinge: Hinge side. wood: Wood species. """ for half, dy in (("lower", 0), ("upper", 1)): self.p(x, y + dy, z, f"{wood}_door", half=half, facing=facing, hinge=hinge, open="false", powered="false") def window(self, x: int, y: int, z: int, facing: str, width: int = 2) -> None: """Install recessed glazing, white lintels and turquoise timber shutters. Args: x, y, z: First glass block. facing: Exterior wall normal. width: Glazed width. """ dx, dz = (1, 0) if facing in ("north", "south") else (0, 1) nx, nz = DIRECTIONS[facing] for i in range(-1, width + 1): px, pz = x + dx * i, z + dz * i self.p(px, y - 1, pz, "smooth_quartz") self.p(px, y + 3, pz, "quartz_bricks") if i in (-1, width): for yy in range(y, y + 3): self.p(px, yy, pz, "stripped_warped_stem", axis="y") self.p(px + nx, yy, pz + nz, "warped_trapdoor", facing=facing, half="bottom", open="true") else: for yy in range(y, y + 3): self.p(px, yy, pz, "light_blue_stained_glass") self.p(px + nx, y - 1, pz + nz, "smooth_quartz_slab", type="top") def planter(self, x: int, y: int, z: int, flower: str = "potted_azalea_bush") -> None: """Place a terracotta planter with supported greenery. Args: x, y, z: Container coordinates. flower: Potted plant state. """ self.p(x, y, z, self.rng.choice(["terracotta", "terracotta", "red_terracotta"])) self.p(x, y + 1, z, flower) def leaf(self, x: int, y: int, z: int, flower: bool = False) -> None: """Place persistent leaves. Args: x, y, z: Leaf coordinates. flower: Use flowering azalea foliage. """ self.p(x, y, z, "flowering_azalea_leaves" if flower else "jungle_leaves", persistent="true", distance="1") def lamp(self, x: int, y: int, z: int) -> None: """Suspend a lantern one chain block below a structural ceiling. Args: x, y, z: Lantern coordinates. """ self.p(x, y + 1, z, "chain", axis="y") self.p(x, y, z, "lantern", hanging="true") def inventory(self, x: int, y: int, z: int, kind: str, items: list) -> None: """Give a placed container a small useful inventory. Args: x, y, z: Container coordinates. kind: Block entity identifier. items: Item identifiers with stack counts, ordered by slot. """ self.block_entities.append(Compound({ "id": String(f"minecraft:{kind}"), "x": Int(x), "y": Int(y), "z": Int(z), "Items": NBTList[Compound]([Compound({ "Slot": Byte(slot), "id": String(f"minecraft:{name}"), "count": Int(count) }) for slot, (name, count) in enumerate(items)]), })) def frame(self, x: int, y: int, z: int, facing: str, item: str) -> None: """Attach a deterministic item-frame entity to the block behind it. Args: x, y, z: Air cell containing the frame. facing: Outward direction. item: Displayed vanilla item. """ dx, dz = DIRECTIONS[facing] self.entities.append(Compound({ "id": String("minecraft:item_frame"), "TileX": Int(x), "TileY": Int(y), "TileZ": Int(z), "Facing": Byte({ "south": 3, "north": 2, "west": 4, "east": 5 }[facing]), "Pos": NBTList[Double]([x + .5 - dx * .46875, y + .5, z + .5 - dz * .46875]), "Motion": NBTList[Double]([0, 0, 0]), "Rotation": NBTList[Float]([{ "south": 0, "west": 90, "north": 180, "east": 270 }[facing], 0]), "UUID": IntArray([self.rng.randrange(-(2**31), 2**31) for _ in range(4)]), "Item": Compound({ "id": String(f"minecraft:{item}"), "count": Int(1) }), "ItemRotation": Byte(0), "ItemDropChance": Float(1), "Fixed": Byte(0), "Invisible": Byte(0), })) def roof(self, x0: int, x1: int, z0: int, z1: int, base: int) -> None: """Build a sealed shallow tiled gable using vanilla terracotta and tile-shaped blocks. Args: x0, x1, z0, z1: Roof footprint including eaves. base: Lowest tile course height. """ # Vanilla has no terracotta stairs; brick and red sandstone supply the half-block tile profile. for z in range(z0, z1 + 1): inset = min(z - z0, z1 - z) y = base + inset // 2 facing = "south" if z <= (z0 + z1) / 2 else "north" for x in range(x0, x1 + 1): for yy in range(base, y): gable = x in (x0, x0 + 1, x1 - 1, x1) self.p(x, yy, z, self.rng.choice(PLASTER) if gable else self.rng.choice(["terracotta", "orange_terracotta", "red_terracotta"])) material = self.rng.choice(["brick"] * 9 + ["red_sandstone"]) if inset % 2: self.p(x, y, z, material + "_stairs", facing=facing, half="bottom", shape="straight") else: self.p(x, y, z, material + "_slab", type="bottom") middle = (z0 + z1) // 2 ridge_y = base + min(middle - z0, z1 - middle) // 2 for x in range(x0, x1 + 1): self.p(x, ridge_y, middle, "terracotta") self.p(x, ridge_y + 1, middle, "brick_slab", type="bottom") for x in range(x0, x1 + 1, 2): self.p(x, base - 1, z0, "spruce_slab", type="top") self.p(x, base - 1, z1, "spruce_slab", type="top") def pergola(self, x0: int, x1: int, z0: int, z1: int, floor: int, beam: int) -> None: """Build an open timber pergola with slatted rafters and a climbing edge. Args: x0, x1, z0, z1: Frame footprint. floor: Deck block height. beam: Beam height. """ for x in (x0, x1): for z in (z0, z1): self.p(x, floor + 1, z, "smooth_quartz") self.fill((x, floor + 2, z, x, beam - 1, z), "stripped_oak_log", axis="y") self.fill((x, beam, z0, x, beam, z1), "stripped_spruce_log", axis="z") for z in range(z0, z1 + 1, 2): self.fill((x0 + 1, beam, z, x1 - 1, beam, z), "oak_slab", type="bottom") for x in range(x0 + 1, x1, 3): self.leaf(x, beam, z0, flower=True) self.leaf(x, beam + 1, z0) for y in range(floor + 2, beam): self.p(x0 - 1, y, z0, "vine", east="true") self.lamp(x1 - 2, beam - 2, z1 - ((z1 - z0) % 2)) def palm(self, x: int, z: int, crown: int) -> None: """Grow a slender palm with stepped fronds attached to its trunk. Args: x, z: Garden trunk coordinates. crown: Crown height. """ self.fill((x, 2, z, x, crown, z), "jungle_log", axis="y") for yy in range(3, crown, 3): self.p(x, yy, z, "stripped_jungle_log", axis="y") self.leaf(x, crown + 1, z) self.leaf(x, crown + 2, z) for dx, dz in DIRECTIONS.values(): for distance in range(1, 4): yy = crown + (1 if distance < 3 else 0) self.leaf(x + dx * distance, yy, z + dz * distance) if distance == 2: self.leaf(x + dx * distance, crown, z + dz * distance) if distance == 3: self.leaf(x + dx * distance, yy - 1, z + dz * distance) for dx, dz in ((-1, -1), (1, -1), (-1, 1), (1, 1)): self.leaf(x + dx, crown, z) self.leaf(x + 2 * dx, crown, z + dz) self.leaf(x + dx, crown, z + dz) self.leaf(x + 2 * dx, crown, z + 2 * dz) self.leaf(x + 2 * dx, crown - 1, z + 2 * dz) for dx in (-1, 1): self.p(x + dx, crown - 1, z, "cocoa", facing="east" if dx < 0 else "west", age="2") def grounds(self) -> None: """Build the raised tiled terrace, square pool, entry stairs and planted garden.""" for x in range(self.size_x): for z in range(self.size_z): if (x < 2 or x > 32) and (z < 2 or z > 28): continue self.p(x, 0, z, "dirt") self.p(x, 1, z, self.rng.choice(["grass_block"] * 12 + ["moss_block", "coarse_dirt"])) self.mix((5, 2, 3, 32, 3, 26), ["diorite"] * 6 + ["calcite", "andesite"]) self.fill((5, 3, 3, 32, 3, 3), "light_blue_terracotta") self.fill((32, 3, 3, 32, 3, 26), "light_blue_terracotta") self.fill((5, 3, 26, 32, 3, 26), "light_blue_terracotta") for x in range(5, 33): for z in range(3, 27): motif = (x % 4 in (0, 1)) and (z % 4 in (0, 1)) self.p(x, 4, z, self.rng.choice(["terracotta", "orange_terracotta", "smooth_red_sandstone"]) if motif else self.rng.choice(["smooth_sandstone"] * 4 + ["cut_sandstone", "sandstone"])) # Pool coping and contained water are flush with the terrace surface. self.mix((22, 1, 21, 30, 3, 29), ["diorite"] * 5 + ["calcite", "andesite"]) self.fill((22, 4, 21, 30, 4, 29), "smooth_quartz") self.fill((23, 2, 22, 29, 2, 28), "prismarine_bricks") for x in range(23, 30): for z in range(22, 29): if (x + z) % 3 == 0: self.p(x, 2, z, "light_blue_terracotta") self.fill((23, 3, 22, 29, 4, 28), "water", level="0") for x, z in ((23, 22), (29, 22), (23, 28), (29, 28)): self.p(x, 2, z, "sea_lantern") for x in (24, 25): self.p(x, 3, 22, "quartz_stairs", facing="north", half="bottom", shape="straight", waterlogged="true") for z in range(27, 30): yy = 31 - z self.fill((16, 2, z, 20, yy, z), "terracotta") for x in range(16, 21): self.p(x, yy, z, "brick_stairs", facing="north", half="bottom", shape="straight") for x in (15, 21): self.fill((x, 2, z, x, yy, z), "diorite") self.p(x, yy + 1, z, "smooth_quartz") # Low balustrades preserve the main garden approach and pool access. for x in range(5, 16): self.p(x, 5, 26, "oak_fence", east="true", west="true") for z in range(18, 27): self.p(5, 5, z, "oak_fence", north="true", south="true") for z in range(16, 27): self.p(32, 5, z, "oak_fence", north="true", south="true") for x, z in ((5, 18), (5, 22), (5, 26), (10, 26), (15, 26), (32, 16), (32, 21), (32, 26)): self.fill((x, 5, z, x, 6, z), "smooth_quartz") self.p(x, 7, z, "brick_slab", type="bottom") for x, z in ((6, 25), (14, 25), (31, 25), (31, 20), (6, 17), (23, 17)): self.planter(x, 5, z, self.rng.choice(["potted_azalea_bush", "potted_flowering_azalea_bush"])) # Lavender and white flowers sit in edged soil beds outside the terrace. for x in range(5, 14): self.p(x, 2, 29, "smooth_sandstone_slab", type="bottom") for z in (27, 28): self.p(x, 1, z, "rooted_dirt") self.p(x, 2, z, "allium" if (x + z) % 2 else "azure_bluet") for z in range(5, 28, 3): self.leaf(33, 2, z) self.p(34, 2, z, "short_grass") for x in (23, 26, 29): self.leaf(x, 2, 30) self.palm(3, 8, 21) self.palm(3, 21, 17) self.p(16, 6, 25, "lantern", hanging="false") self.p(16, 5, 25, "sandstone_wall") def structure(self) -> None: """Build both storeys, the enchanting wing and a continuous internal staircase.""" for y0, y1 in ((5, 9), (11, 15)): for bounds in ((7, y0, 4, 24, y1, 4), (7, y0, 16, 24, y1, 16), (7, y0, 5, 7, y1, 15), (24, y0, 5, 24, y1, 15)): self.mix(bounds, PLASTER) self.mix((8, 4, 5, 23, 4, 15), FLOOR) self.fill((7, 10, 4, 24, 10, 16), "smooth_quartz") self.mix((8, 10, 5, 23, 10, 15), FLOOR) self.fill((7, 16, 4, 24, 16, 16), "smooth_quartz") for y in (5, 11): self.fill((7, y, 4, 24, y, 4), "light_blue_terracotta") self.fill((7, y, 16, 24, y, 16), "light_blue_terracotta") self.fill((7, y, 4, 7, y, 16), "light_blue_terracotta") self.fill((24, y, 4, 24, y, 16), "light_blue_terracotta") for x in (7, 24): for z in (4, 16): self.fill((x, 5, z, x, 15, z), "quartz_pillar", axis="y") # Ground-floor circulation: living room left, hallway middle, library wing right. self.fill((17, 5, 5, 17, 9, 15), "white_concrete") self.door(17, 5, 7, "east", wood="oak") self.mix((25, 4, 9, 30, 4, 14), FLOOR) for bounds in ((25, 5, 8, 31, 9, 8), (31, 5, 9, 31, 9, 15), (25, 5, 15, 30, 9, 15)): self.mix(bounds, PLASTER) self.fill((24, 10, 8, 31, 10, 15), "smooth_quartz") self.door(24, 5, 11, "east", wood="warped") # Six rises connect feet elevations 5 and 11. The upper landing is at z=15. self.fill((15, 10, 11, 16, 10, 14), "air") for z in range(9, 15): yy = z - 4 if yy > 5: self.fill((15, 5, z, 16, yy - 1, z), "oak_planks") for x in (15, 16): self.p(x, yy, z, "oak_stairs", facing="south", half="bottom", shape="straight") # Upstairs plan preserves the kitchen on the left and brew/bedroom across the back. self.fill((14, 11, 5, 14, 15, 10), "white_concrete") self.fill((19, 11, 5, 19, 15, 10), "white_concrete") self.fill((15, 11, 10, 23, 15, 10), "white_concrete") self.door(14, 11, 8, "east", wood="birch") self.door(18, 11, 10, "south", wood="birch") self.door(22, 11, 10, "south", wood="birch") for x in (14, 17): for z in range(11, 15): self.p(x, 11, z, "oak_fence", north="true", south="true") # Front entrance, with a taller recessed stone surround. for x, hinge in ((21, "left"), (22, "right")): self.door(x, 5, 16, "south", hinge) self.p(x, 7, 16, "light_blue_stained_glass") for x in (20, 23): self.fill((x, 5, 17, x, 8, 17), "smooth_sandstone") self.p(x, 9, 17, "chiseled_sandstone") self.fill((20, 9, 17, 23, 9, 17), "smooth_sandstone") self.fill((21, 8, 17, 22, 8, 17), "smooth_sandstone") for x in (20, 23): self.planter(x, 5, 18, "potted_flowering_azalea_bush") # Cool blue plaster panels recall the reference's blue-and-white elevations. for bounds in ((7, 11, 5, 7, 15, 15), (24, 11, 5, 24, 15, 15), (8, 11, 16, 12, 15, 16), (31, 5, 9, 31, 9, 14), (25, 5, 15, 30, 9, 15)): self.mix(bounds, ["light_blue_concrete"] * 9 + ["light_blue_terracotta"]) self.window(10, 6, 16, "south") self.window(10, 12, 16, "south") for x, hinge in ((14, "left"), (15, "right")): self.door(x, 11, 16, "south", hinge) self.p(x, 13, 16, "light_blue_stained_glass") self.window(20, 12, 16, "south") for y in (6, 12): self.window(10, y, 4, "north") self.window(7, y, 7, "west") self.window(7, y, 12, "west") self.window(20, 6, 4, "north") self.window(16, 12, 4, "north") self.window(21, 12, 4, "north", width=1) self.window(24, 12, 7, "east") self.window(27, 6, 15, "south") self.window(31, 6, 11, "east") self.window(27, 6, 8, "north") # Upper terrace, with real support columns and open railings. self.fill((7, 10, 17, 16, 10, 21), "oak_planks") for x in (7, 16): self.fill((x, 5, 21, x, 9, 21), "quartz_pillar", axis="y") self.fill((7, 9, 21, 16, 9, 21), "smooth_quartz") for x in range(8, 16): self.p(x, 11, 21, "oak_fence", east="true", west="true") for z in range(17, 21): self.p(7, 11, z, "oak_fence", north="true", south="true") self.p(16, 11, z, "oak_fence", north="true", south="true") self.pergola(7, 16, 17, 21, 10, 15) self.pergola(25, 32, 17, 20, 4, 10) self.roof(6, 25, 3, 17, 17) self.roof(25, 32, 7, 16, 11) self.fill((9, 19, 7, 9, 24, 7), "quartz_pillar", axis="y") self.p(9, 25, 7, "brick_slab", type="bottom") # Supported wall climbers stay on piers, away from glass and door openings. for x, z, facing, bottom, top in ((18, 17, "north", 5, 14), (6, 10, "east", 5, 13), (25, 5, "west", 5, 13)): for yy in range(bottom, top + 1): # Vine face points toward its supporting wall. support = self.get(x + DIRECTIONS[facing][0], yy, z + DIRECTIONS[facing][1]) if support and support != "minecraft:air": self.p(x, yy, z, "vine", **{facing: "true"}) for x in (8, 9, 12, 13): self.leaf(x, 9, 22, flower=x % 2 == 0) self.p(x, 8, 22, "vine", up="true") for x in (18, 19): self.leaf(x, 10, 17, flower=True) self.p(x, 9, 17, "vine", north="true") def interiors(self) -> None: """Furnish all seven named rooms with usable furniture, storage and lighting.""" # Living room: patterned rug, dining table, sofa, cabinets and a reading corner. for x in range(9, 15): for z in range(6, 11): self.p(x, 5, z, "orange_carpet" if (x + z) % 3 == 0 else "brown_carpet") for x in range(10, 13): for z in (7, 8): self.p(x, 5, z, "oak_fence", east="true", west="true") self.p(x, 6, z, "birch_slab", type="top") self.p(11, 7, 7, "white_candle", candles="3", lit="true") self.p(12, 7, 8, "potted_fern") for x in (10, 11, 12): self.p(x, 5, 5, "birch_stairs", facing="north", half="bottom", shape="straight") self.p(x, 5, 10, "oak_stairs", facing="south", half="bottom", shape="straight") self.fill((8, 5, 5, 8, 6, 6), "bookshelf") self.p(8, 7, 5, "potted_blue_orchid") self.p(8, 5, 14, "barrel", facing="south") self.p(8, 6, 14, "lantern", hanging="false") self.inventory(8, 5, 14, "barrel", [("bread", 12), ("apple", 6)]) self.p(10, 5, 14, "chest", facing="south", type="single") self.inventory(10, 5, 14, "chest", [("book", 8), ("paper", 16)]) self.frame(9, 7, 15, "north", "painting") self.lamp(11, 8, 8) # Ground hallway stays clear across the front door and both room doors. for z in range(7, 15): self.p(21, 5, z, "light_blue_carpet") self.p(18, 5, 5, "barrel", facing="south") self.p(18, 6, 5, "potted_bamboo") self.inventory(18, 5, 5, "barrel", [("torch", 24), ("lead", 2)]) self.p(23, 5, 14, "chest", facing="west", type="single") self.inventory(23, 5, 14, "chest", [("oak_boat", 1), ("fishing_rod", 1)]) self.frame(18, 7, 5, "south", "clock") self.lamp(21, 8, 8) # Enchanting room: two-high shelves with a one-block air gap around the table. shelves = {(x, z) for x in range(26, 31) for z in (9, 13)} | {(x, z) for x in (26, 30) for z in range(10, 13)} shelves -= {(26, 11), (28, 13)} for x, z in sorted(shelves): self.fill((x, 5, z, x, 6, z), "bookshelf") self.p(28, 5, 11, "enchanting_table") # Enchanting gaps remain air for bookshelf power; the rug lives at the entry. self.p(25, 5, 11, "purple_carpet") self.p(25, 5, 14, "lectern", facing="east", has_book="true") self.block_entities.append(Compound({"id": String("minecraft:lectern"), "x": Int(25), "y": Int(5), "z": Int(14), "Page": Int(0), "Book": Compound({"id": String("minecraft:written_book"), "count": Int(1), "components": Compound({"minecraft:written_book_content": Compound({"title": String("Villa al Mare"), "author": String("The Librarian"), "pages": NBTList[Compound]([Compound({"raw": String('{"text":"Notes on sea, stars and enchantments."}')})])})})})})) self.p(30, 5, 14, "chest", facing="west", type="single") self.inventory(30, 5, 14, "chest", [("lapis_lazuli", 32), ("book", 12), ("amethyst_shard", 8)]) self.p(26, 7, 9, "white_candle", candles="3", lit="true") self.p(30, 7, 13, "potted_azalea_bush") self.frame(30, 8, 14, "north", "amethyst_shard") self.lamp(28, 8, 11) # Kitchen: L-shaped stone counters, a stocked pantry and smoker with hood. for x in range(8, 13): self.p(x, 11, 5, "smooth_quartz") for z in range(6, 10): self.p(8, 11, z, "smooth_quartz") self.p(8, 11, 6, "smoker", facing="east", lit="false") self.inventory(8, 11, 6, "smoker", [("potato", 8), ("coal", 16)]) self.fill((8, 14, 6, 8, 15, 6), "smooth_quartz") self.p(10, 11, 5, "water_cauldron", level="3") self.p(11, 12, 5, "potted_fern") self.p(8, 12, 9, "cake", bites="0") self.fill((8, 11, 14, 8, 12, 14), "barrel", facing="east") self.inventory(8, 11, 14, "barrel", [("carrot", 16), ("wheat", 32), ("sugar", 8)]) self.inventory(8, 12, 14, "barrel", [("bowl", 6), ("beetroot", 16)]) self.p(10, 11, 12, "oak_fence") self.p(10, 12, 12, "birch_slab", type="top") self.p(10, 13, 12, "white_candle", candles="2", lit="true") self.p(10, 11, 14, "birch_stairs", facing="south", half="bottom", shape="straight") self.p(12, 11, 12, "yellow_carpet") self.frame(8, 13, 10, "east", "bread") self.lamp(11, 14, 10) # Brewing room along the north wall of the second floor. for z in range(5, 9): self.p(18, 11, z, "barrel", facing="west") for z in (6, 7): self.p(18, 12, z, "brewing_stand", has_bottle_0="true", has_bottle_1="true", has_bottle_2="true") self.inventory(18, 12, z, "brewing_stand", [("potion", 1), ("potion", 1), ("potion", 1), ("nether_wart", 6), ("blaze_powder", 16)]) self.inventory(18, 11, 5, "barrel", [("glass_bottle", 16), ("nether_wart", 24), ("redstone", 12)]) self.p(15, 11, 5, "water_cauldron", level="3") self.p(15, 11, 9, "chest", facing="east", type="single") self.inventory(15, 11, 9, "chest", [("glowstone_dust", 16), ("spider_eye", 8)]) self.p(16, 11, 7, "blue_carpet") self.p(18, 12, 8, "potted_red_mushroom") self.frame(15, 13, 5, "south", "blaze_powder") self.lamp(17, 14, 7) # Bedroom: complete double bed, tall timber wardrobe and bedside light. for x in (20, 21): self.p(x, 11, 8, "cyan_bed", part="foot", facing="north", occupied="false") self.p(x, 11, 7, "cyan_bed", part="head", facing="north", occupied="false") self.fill((x, 11, 6, x, 12, 6), "stripped_oak_log", axis="y") for x in (22, 23): self.fill((x, 11, 5, x, 13, 5), "barrel", facing="south") for y in range(11, 14): self.p(x, y, 6, "spruce_trapdoor", facing="south", open="true", half="bottom") self.inventory(22, 11, 5, "barrel", [("leather_helmet", 1), ("leather_chestplate", 1)]) self.p(20, 11, 5, "barrel", facing="south") self.p(20, 12, 5, "lantern", hanging="false") self.p(22, 11, 8, "cyan_carpet") self.p(22, 11, 9, "cyan_carpet") self.frame(20, 12, 7, "south", "nautilus_shell") self.lamp(21, 14, 8) # Upper hall and landing have a rug, writing desk and a supported potted plant. self.fill((20, 11, 12, 22, 11, 14), "cyan_carpet") self.p(23, 11, 11, "barrel", facing="west") self.p(23, 12, 11, "potted_blue_orchid") self.inventory(23, 11, 11, "barrel", [("map", 1), ("compass", 1)]) self.frame(18, 13, 15, "north", "compass") self.lamp(21, 14, 13) def outdoors(self) -> None: """Furnish both terraces with dining furniture, loungers and planted railings.""" # Dining beneath the upper balcony. for x in (9, 10): for z in (19, 20): self.p(x, 5, z, "oak_fence") self.p(x, 6, z, "birch_slab", type="top") self.p(9, 7, 19, "potted_blue_orchid") self.p(10, 7, 20, "white_candle", candles="2", lit="true") for z in (19, 20): self.p(8, 5, z, "oak_stairs", facing="west", half="bottom", shape="straight") self.p(11, 5, z, "oak_stairs", facing="east", half="bottom", shape="straight") self.lamp(13, 8, 19) # Upper balcony sofa and coffee table; the double doors remain accessible. for x in (9, 10, 11): self.p(x, 11, 18, "birch_stairs", facing="north", half="bottom", shape="straight") self.p(12, 11, 19, "oak_planks") self.p(12, 12, 19, "potted_cactus") self.planter(8, 11, 20, "potted_azalea_bush") self.planter(15, 11, 20, "potted_flowering_azalea_bush") # Two cyan sun loungers, set back from the pool under the lower pergola. for x in (27, 30): self.p(x, 5, 17, "quartz_stairs", facing="north", half="bottom", shape="straight") self.p(x, 5, 18, "cyan_wool") self.p(x, 5, 19, "smooth_quartz_slab", type="top") self.p(x, 6, 18, "cyan_carpet") self.p(28, 5, 18, "oak_planks") self.p(28, 6, 18, "potted_cactus") self.p(31, 5, 23, "barrel", facing="up") self.p(31, 6, 23, "lantern", hanging="false") self.inventory(31, 5, 23, "barrel", [("tropical_fish_bucket", 1), ("fishing_rod", 1)]) for x in range(27, 32): self.leaf(x, 5, 16, flower=x % 3 == 0) def build(self) -> "Build": """Construct the reference-inspired Mediterranean villa. Returns: Build: This instance, ready to export. """ self.grounds() self.structure() self.interiors() self.outdoors() return self def export(self, path: Path) -> Path: """Write the schematic so that two runs produce identical bytes. Args: path (Path): Destination .litematic path. Returns: Path: The path written. """ schematic = self.canvas.to_litematica( name=CONFIG["name"], author=CONFIG["author"], description=CONFIG["description"], minecraft_data_version=CONFIG["data_version"], ) # The metadata otherwise records the current wall clock, which changes the bytes on every run. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 path.parent.mkdir(parents=True, exist_ok=True) root = schematic.write_to_nbt() region = root["Regions"][CONFIG["name"]] region["Entities"] = NBTList[Compound](self.entities) region["TileEntities"] = NBTList[Compound](self.block_entities) NBTFile(root).save(path) return path def verify(self, path: Path) -> Dict[str, object]: """Reload the written file and confirm it matches what was placed. Args: path (Path): Schematic to reload. Returns: Dict[str, object]: Size, block count and palette size of the reloaded file. Raises: AssertionError: When a reloaded cell differs from what was placed. """ loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload" ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for (x, y, z), state in self.placed.items(): assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload" root = NBTFile.load_regardless_of_gzipped(path) assert len(root["Regions"]) == 1, "export must have exactly one region" region = root["Regions"][CONFIG["name"]] assert region["Entities"] == NBTList[Compound](self.entities), "item frames changed on reload" assert region["TileEntities"] == NBTList[Compound](self.block_entities), "furnished block entities changed on reload" return { "size_xyz": [size_x, size_y, size_z], "placed_blocks": int(root["Metadata"]["TotalBlocks"]), "palette_states": len(loaded.palette), "item_frames": len(self.entities), "block_entities": len(self.block_entities), } def main() -> None: """Generate the build, export it, and verify the exported file.""" parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.") parser.add_argument("--output", type=Path, default=Path(CONFIG["output"])) parser.add_argument("--seed", type=int, default=CONFIG["seed"]) args = parser.parse_args() build = Build(seed=args.seed).build() path = build.export(args.output) report = build.verify(path) print(f"wrote {path}: {report}") if __name__ == "__main__": main()
mediterranean-villa/render.mjs — 72 行
// Render four exterior inspection views of the finished Mediterranean villa. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createRenderScene, renderLitematicWithMetadata } from '/work/generator/MCRender/src/headless/index.ts'; const RENDER_ROOT = '/work/generator/MCRender'; const requireRenderer = createRequire(path.join(RENDER_ROOT, 'package.json')); const { PNG } = requireRenderer('pngjs'); // vite-node passes script arguments after a bare "--", so drop it before reading them. const argv = process.argv.slice(2).filter(entry => entry !== '--'); const INPUT = argv[0] ?? 'output.litematic'; const OUT_DIR = argv[1] ?? 'previews'; const SIZE = Number(process.env.RENDER_SIZE ?? 900); // Tuned defaults. Ambient near 0.72 keeps shaded faces readable, and a light direction with a // dominant +Y term plus a small +X/+Z tilt separates roof, wall and ground planes. const LIGHT_DIRECTION = [0.08, 0.28, 0.15]; const AMBIENT = 0.72; const BACKGROUND = [157, 208, 224, 255]; const VIEWS = [ { name: 'front', azimuth: 35, elevation: 29, radius: 3.5 }, { name: 'rear', azimuth: 210, elevation: 26, radius: 3.5 }, { name: 'side', azimuth: 135, elevation: 27, radius: 3.5 }, { name: 'top', azimuth: 35, elevation: 65, radius: 3.5 }, ]; // 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})`); }

解题方式

build第几个请求首次渲染第几个请求首次测试 渲染请求数测试请求数留下的渲染图
root-house69 477
sandstone-cottage711 346
stone-cottage916 436
red-roof-farmstead510 536
stilt-water-cabin914 445
stilted-manor49 578
merchant-manor711 699
mediterranean-villa712 547

共同骨架。八个 run 都走同一条路:先 rg 列文件、cat 读 AGENTS.md 和两个模板, 把模板复制成 generate.pyrender.mjs;接着去 blocks.json 里查这次要用的方块和它们的 state 合法值;然后把建造方法写成一个大块、拼进 Build 类;跑一次生成,渲染四视图, 用 cutaway.py 切开看室内;之后才开始写测试,并在测试失败与改生成器之间来回,直到全绿。

分歧一:怎么把代码写进文件。七个 run 用 shell heredoc——先把方法体写到 /tmp/*.py, 再用一段 python 把它 splice 进 generate.py。只有 stone-cottage 用了 CLI 自带的 patch 工具, 直接下 unified diff。后者在 trace 里看不到命令,只看到 FileChange 记录, 也因此它的『写生成器』阶段吃掉了 27k 新增输入,是八个里最高的。

分歧二:先看图还是先测试。八个都是先渲染后测试,但间隔差别很大: merchant-manor 在第 10 个请求就开始写测试,之后有 11 个请求在测试上; stilted-manor 拖到第 9 个请求,但渲染请求多达 6 次,它更依赖看图定形。 看图多的那几个(stilted-manor、mediterranean-villa、merchant-manor)也正好是留下渲染图最多的。

分歧三:室内的处理。prompt 只给一张外观图,室内全靠模型自己设计。 八个 run 都用 cutaway.py 切层看内部,说明『看不见的地方要自己检查』这条已经内化; 但只有一半在测试里写了家具与内饰相关的断言,另一半靠渲染肉眼过。

共同的收尾。八个 run 的最后一个请求都是纯文字交付说明,格式高度一致:一句完成, 一组指向产物、生成器、测试、渲染图的链接,一句『所有测试通过』,一句『没有未完成项』。 这是 PROMPT.md 里没要求的格式,模型自己固定下来的。


harness 改进点

  1. 把重复写格拦在 put() 里。三个 build 撞上 changed on reload, 原因都是同一坐标写两次,而错误要等到导出并重载之后才暴露。模板应当在写入时就报, 并把两次写入的调用点都打出来。这是本轮唯一由工具缺陷直接造成的失败类别。
  2. 给『测试预算』一个显式提示。测试行数在八个任务上几乎是常数,和任务规模无关。 如果大建筑本该配更多测试,就得在 AGENTS.md 里按规模给出建议数量,否则模型会一直按固定配额写。
  3. cutaway.py 的用法提到更前面。八个 run 都用了它,但都是在第一次渲染之后才想起来, 说明它现在的位置偏后。室内是这一轮的核心要求,切片应该和四视图渲染并列成为默认动作。
  4. 缓存命中 93.8% 意味着上下文重复喂了 15 倍。真正能压的是每轮重复贴进去的文件内容。 如果模板自带一份精简的 API 速查(而不是让模型 cat 整个 sketch.py), 『探索』阶段的新增输入还能再降。
  5. 按 toolset 记录产物来源。这一轮八个 run 用的是同一套工具,但记录里并没有写下是哪一套。 已经改掉:report.json 现在记 toolset 名与每个工具文件的 sha256, workspace_code 标每个文件是 seeded / edited / agent,统计模型代码量时按 diff 算。
  6. 渲染与测试的分工已经稳定,可以固化成检查单。渲染抓比例、材质、檐口穿插; 测试抓可达性、封闭性、悬空、确定性。这一轮八个 build 的测试分类分布几乎完全落在后四类上, 可以直接把这四类写成 AGENTS.md 的最低要求,把渲染要看的三样写成另一张单子。