Contents

第三轮:拿掉参考图


同一个任务、同一个容器、同一批模板,只做一处改动:不给参考图,把建筑要求全部写成文字。 四档全部通过,代价降到前两轮的三分之二,但产物的材质丰富度掉了一截。

$1.57四档合计(上轮 $2.37)
-43%总 token
-31%总耗时
4/4通过

关键观察

  1. 无图材质丰富度下降, 审美与设计感下降, 费用和时间下降 50%~30% 不等
  2. 结构正确性测试减少了, 但是结构正确性不变
89/80/102/114 → 63/77/82/100palette state
$2.37 → $1.57四档合计费用
782 → 693测试体行数
4/4仍然全部通过

费用与时间的降幅按档位分别是:low −58% 费用 / −56% 时间,medium −27% / −24%, high −26% / −18%,xhigh −31% / −30%。


实验设置

容器、模板、打分器与上一轮完全相同,唯一变化在 prompt 目录: 新建 prompts/lodge-noimage/,删掉参考图与整个 ## References 段, 其余逐字保留,只把四处"参照图片"的指代改成直述。task/ 为空,所以第一轮对话没有任何附件。

./run_agent.sh lodge-noimage --model gpt-6-astra --effort low --timeout 5400 --memory 4g --cpus 3 ./run_agent.sh lodge-noimage --model gpt-6-astra --effort medium --timeout 5400 --memory 4g --cpus 3 ./run_agent.sh lodge-noimage --model gpt-6-astra --effort high --timeout 5400 --memory 4g --cpus 3 ./run_agent.sh lodge-noimage --model gpt-6-astra --effort xhigh --timeout 5400 --memory 4g --cpus 3
# Task: Amberstone Lodge Build the lodge described below as a furnished Minecraft Java 1.21.1 schematic, and export it to `output.litematic`. Write a generator script that produces it, and verify the result. ## Build requirements Structure and scale - Java 1.21.1, vanilla blocks, one region, one schematic. - Scale: roughly 45 by 35 blocks of ground and about 40 blocks tall. Keep the footprint at that scale so the interiors stay walkable. - Two masses: a tall steep gabled wing on one side, a lower wing beside it, joined without gaps where the roofs meet. - Ground level is stone, upper levels are plaster panels framed with timber. Roofs are dark, deepslate family. Vary each material with two or three related blocks so no surface is a single flat texture. Exterior detail - A covered blacksmith area on the lower wing with a chimney, a working forge (lit), an anvil and storage. - A raised stone terrace with steps and an irregular pathway leading away from the entrance. - At least two hand built pine trees: trunk, stepped branches, layered leaves. - Windows with frames and shutters, wall lanterns, and climbing plants on part of the stonework. Interiors - At least four levels or distinct room groups, all reachable on foot from the front door: entrance and workshop or storage at ground level, a living and dining floor, a bedroom, and an attic study. - The bedroom has a green bed on a raised platform, striped curtain banners at the window, wall cabinets, a bookshelf, and lanterns. - Furnish every room with barrels, chests, item frames, carpets, flower pots, candles, ladders and stairs used as furniture. - Every room has a light source within a few blocks of its floor. Correctness, not just looks - Every floor is connected by stairs or ladders, 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. ## 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. Starting from `/opt/tools/template_generator.py` gives you bounds checked placement, a seeded random source and a byte reproducible export for free. - `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.
# Task: Amberstone Lodge Build the lodge shown in the two reference images as a furnished Minecraft Java 1.21.1 schematic, and export it to `output.litematic`. Write a generator script that produces it, and verify the result. ## References - `reference_exterior.png`: the lodge from the front left. Annotations point at the blacksmith area, the custom pine trees, and the stone pathway. - `reference_interiors.png`: three interior shots. Top: bedroom with a green bed, striped curtains, wall cabinets and a lantern. Bottom left: study corner with candles, barrels and an open book. Bottom right: hallway with a glass door, a bookshelf wall, climbing plants and a stone chimney breast. The references show the front and three interior corners. Reconstruct the hidden sides and rooms so they stay consistent with what is visible. ## Build requirements Structure and scale - Java 1.21.1, vanilla blocks, one region, one schematic. - Reference scale: roughly 45 by 35 blocks of ground and about 40 blocks tall. Keep the footprint at that scale so the interiors stay walkable. - Two masses, as in the reference: a tall steep gabled wing on one side, a lower wing beside it, joined without gaps where the roofs meet. - Ground level is stone, upper levels are plaster panels framed with timber. Roofs are dark, deepslate family. Vary each material with two or three related blocks so no surface is a single flat texture. Exterior detail - A covered blacksmith area on the lower wing with a chimney, a working forge (lit), an anvil and storage. - A raised stone terrace with steps and an irregular pathway leading away from the entrance. - At least two hand built pine trees: trunk, stepped branches, layered leaves. - Windows with frames and shutters, wall lanterns, and climbing plants on part of the stonework. Interiors - At least four levels or distinct room groups, all reachable on foot from the front door: entrance and workshop or storage at ground level, a living and dining floor, a bedroom, and an attic study. - The bedroom must match the reference: a green bed on a raised platform, striped curtain banners at the window, wall cabinets, a bookshelf, and lanterns. - Furnish every room with the block combinations the references use: barrels, chests, item frames, carpets, flower pots, candles, ladders and stairs used as furniture. - Every room has a light source within a few blocks of its floor. Correctness, not just looks - Every floor is connected by stairs or ladders, 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. ## 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. Starting from `/opt/tools/template_generator.py` gives you bounds checked placement, a seeded random source and a byte reproducible export for free. - `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.

对比表

这一轮的实测值,最后一列是相对上一轮的变化。点表头排序。

effort耗时 s请求total tokenoutputreasoning 命令失败命令方块尺寸 XYZpalette种类 生成器行数测试数渲染张数估算费用对比上轮
low34513 360,3479,005692 13212,03845x40x35 63474327 6$0.183 -71% token / -56% 时间
medium60819 770,83917,9524,310 15110,42145x40x35 77584847 8$0.326 -38% token / -24% 时间
high78520 948,89523,4646,198 29311,60245x40x35 826154911 9$0.430 -44% token / -18% 时间
xhigh120023 1,464,59336,07514,171 26211,86745x40x35 1007571812 8$0.630 -30% token / -30% 时间

三轮并排

每格是「第一轮 / 修复后 / 无图」。

effort耗时 stotal token命令方块palette 生成器行数测试数估算费用
low667 / 776 / 3451,203,717 / 1,238,036 / 360,34726 / 25 / 1310,031 / 10,599 / 12,03874 / 89 / 63354 / 575 / 4328 / 9 / 7$0.41 / $0.44 / $0.18
medium735 / 802 / 6081,407,614 / 1,240,852 / 770,83924 / 36 / 1510,661 / 10,218 / 10,42179 / 80 / 77397 / 536 / 4849 / 7 / 7$0.46 / $0.45 / $0.33
high1568 / 960 / 7852,061,936 / 1,702,412 / 948,89533 / 28 / 2912,341 / 11,946 / 11,602101 / 102 / 82614 / 754 / 54912 / 10 / 11$0.66 / $0.58 / $0.43
xhigh1669 / 1719 / 12002,443,140 / 2,082,834 / 1,464,59332 / 30 / 2611,992 / 10,654 / 11,867126 / 114 / 100915 / 899 / 71812 / 14 / 12$1.01 / $0.91 / $0.63

图表

每张图一个量纲,三根柱子是三轮。

Fig 1. 墙钟耗时(秒)。
Fig 2. 总 token。
Fig 3. 模型请求数与 shell 命令数(次)。请求数的下降幅度最大,这是省钱的主因。
Fig 4. 非空气方块数(个)。三轮基本持平,房子一样大。
Fig 5. palette state 与方块种类(个)。这是拿掉图之后掉得最明显的指标。
Fig 6. 估算费用(美元)。

schematic

左右成对:左为这一轮(无图),右为上一轮(有图)。可拖动旋转。

low · 无图 · 12,038 方块 · palette 63

low · 有图(修复后) · 10,599 方块 · palette 89

medium · 无图 · 10,421 方块 · palette 77

medium · 有图(修复后) · 10,218 方块 · palette 80

high · 无图 · 11,602 方块 · palette 82

high · 有图(修复后) · 11,946 方块 · palette 102

xhigh · 无图 · 11,867 方块 · palette 100

xhigh · 有图(修复后) · 10,654 方块 · palette 114


渲染图

effort = low

无图这一轮的渲染(6 张)
low_bedroom.png
bedroom.png
low_front.png
front.png
low_living.png
living.png
low_rear.png
rear.png
low_side.png
side.png
low_top.png
top.png
有图那一轮的渲染(22 张)
low_bedroom_detail.png
bedroom_detail.png
low_front.png
front.png
low_ground_detail.png
ground_detail.png
low_rear.png
rear.png
low_side.png
side.png
low_study_detail.png
study_detail.png
low_top.png
top.png
low_workshop_detail.png
workshop_detail.png

effort = medium

无图这一轮的渲染(8 张)
medium_attic.png
attic.png
medium_bedroom.png
bedroom.png
medium_front.png
front.png
medium_ground.png
ground.png
medium_living.png
living.png
medium_rear.png
rear.png
medium_side.png
side.png
medium_top.png
top.png
有图那一轮的渲染(11 张)
medium_bedroom.png
bedroom.png
medium_front.png
front.png
medium_front_left.png
front_left.png
medium_ground.png
ground.png
medium_living.png
living.png
medium_rear.png
rear.png
medium_side.png
side.png
medium_study.png
study.png
medium_top.png
top.png

effort = high

无图这一轮的渲染(9 张)
high_attic_study.png
attic_study.png
high_bedroom.png
bedroom.png
high_entrance_workshop.png
entrance_workshop.png
high_front.png
front.png
high_living_dining.png
living_dining.png
high_rear.png
rear.png
high_side.png
side.png
high_stair_section.png
stair_section.png
high_top.png
top.png
有图那一轮的渲染(11 张)
high_attic_study.png
attic_study.png
high_bedroom.png
bedroom.png
high_front.png
front.png
high_ground_rooms.png
ground_rooms.png
high_living_dining.png
living_dining.png
high_low_loft.png
low_loft.png
high_rear.png
rear.png
high_side.png
side.png
high_top.png
top.png

effort = xhigh

无图这一轮的渲染(8 张)
xhigh_attic_cutaway.png
attic_cutaway.png
xhigh_bedroom_cutaway.png
bedroom_cutaway.png
xhigh_front.png
front.png
xhigh_ground_cutaway.png
ground_cutaway.png
xhigh_living_cutaway.png
living_cutaway.png
xhigh_rear.png
rear.png
xhigh_side.png
side.png
xhigh_top.png
top.png
有图那一轮的渲染(12 张)
xhigh_bedroom.png
bedroom.png
xhigh_bedroom_plan.png
bedroom_plan.png
xhigh_forge.png
forge.png
xhigh_front.png
front.png
xhigh_ground.png
ground.png
xhigh_living.png
living.png
xhigh_rear.png
rear.png
xhigh_side.png
side.png
xhigh_study.png
study.png
xhigh_top.png
top.png

命令轨迹

每条 shell 命令的首行、退出码、耗时。

low — 13 条命令,2 条失败
[01] exit=0 0s cat /work/AGENTS.md; cat /opt/tools/template_generator.py; cat /opt/tools/template_render.mjs [01] exit=0 0s rg -n 'entities|block_entities|tile_entities' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/schematic*; rg -n 'class .*|entities|block [02] exit=1 0s cp /opt/tools/template_generator.py generate.py <-- FAILED [03] exit=2 0s sed -n '636,770p' /work/generator/MCIO/mcio/litematica.py; rg -n 'to_litematica|entities' /work/generator/MCIO/mcio/sketch -g '*.py' <-- FAILED [03] exit=0 0s rg -n 'def save|TileEntities|Entities' /work/generator/MCIO/mcio/litematica.py; sed -n '1,110p' /work/generator/MCIO/mcio/nbt/nbt.py [04] exit=0 0s sed -n '1319,1345p' /work/generator/MCIO/mcio/litematica.py [06] exit=0 0s cat > /tmp/build_body.txt <<'EOF' [07] exit=0 3s chmod u+w generate.py render.mjs [08] exit=0 0s python - <<'PY' [09] exit=0 1s python - <<'PY' [10] exit=0 5s /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/bedroom-slice.litematic --max-y 22 [10] exit=0 0s python - <<'PY' [12] exit=0 3s python - <<'PY'
medium — 15 条命令,1 条失败
[01] exit=0 0s pwd && cat /work/AGENTS.md && cat /opt/tools/template_generator.py && cat /opt/tools/template_render.mjs [01] exit=0 0s rg -n 'entity|block_entity' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/schematic/litematica.py; rg --files /work/generator/MCIO/mci [02] exit=0 0s rg -n 'entities|tile_entities|class Region|class Litematica' /work/generator/MCIO/mcio/litematica.py; cat /work/generator/MCIO/mcio/sketch.py | tail - [04] exit=0 0s sed -n '636,850p' /work/generator/MCIO/mcio/litematica.py; head -100 /work/generator/MCIO/mcio/nbt/nbt.py; /opt/venv/bin/python - <<'PY' [05] exit=0 0s cat > /tmp/build_body.txt <<'PY' [06] exit=0 2s chmod u+w generate.py render.mjs [07] exit=0 0s /opt/venv/bin/python - <<'PY' [08] exit=0 0s /opt/venv/bin/python - <<'PY' [09] exit=1 1s mkdir -p test <-- FAILED [11] exit=0 0s /opt/venv/bin/python - <<'PY' [12] exit=0 1s /opt/venv/bin/python - <<'PY' [13] exit=0 4s /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/living-slice.litematic --max-y 13 [14] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/bedroom-slice.litematic [15] exit=0 1s /opt/venv/bin/python - <<'PY' [17] exit=0 4s /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/ground-slice.litematic --max-y 6
high — 29 条命令,3 条失败
[01] exit=0 0s pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' . /work 2>/dev/null | head -60 [01] exit=0 0s cat /work/AGENTS.md /opt/tools/template_generator.py /opt/tools/template_render.mjs [02] exit=0 0s cp /opt/tools/template_generator.py generate.py [03] exit=0 0s rg --files /work/generator/MCIO | head -50 [04] exit=0 0s rg -n "entit|to_litematica" /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/litematica.py [04] exit=0 0s sed -n '90,155p' /work/generator/MCIO/mcio/sketch.py [06] exit=0 0s cat > /tmp/lodge_methods.txt <<'PY' [07] exit=1 0s chmod u+w generate.py render.mjs <-- FAILED [08] exit=0 0s /opt/venv/bin/python - <<'PY' [08] exit=0 2s RENDER_SIZE=768 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [09] exit=0 0s /opt/venv/bin/python - <<'PY' [11] exit=1 1s /opt/venv/bin/python - <<'PY' <-- FAILED [12] exit=1 1s /opt/venv/bin/python - <<'PY' <-- FAILED [13] exit=0 1s cat /opt/tools/cutaway.py | head -130 [14] exit=0 0s cat > preview_slices.py <<'PY' [14] exit=0 8s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/bedroom.litematic --outp [14] exit=0 8s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/attic_study.litematic -- [14] exit=0 8s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/entrance_workshop.litema [14] exit=0 8s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/living_dining.litematic [14] exit=0 8s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/stair_section.litematic [14] exit=0 9s RENDER_SIZE=768 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [15] exit=0 1s /opt/venv/bin/python - <<'PY' [17] exit=0 1s /opt/venv/bin/python - <<'PY' [17] exit=0 3s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/bedroom.litematic --outp [17] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/living_dining.litematic [17] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/entrance_workshop.litema [17] exit=0 3s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/stair_section.litematic [17] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/attic_study.litematic -- [18] exit=0 0s du -h output.litematic generate.py test/test_structure.py previews/*.png
xhigh — 26 条命令,2 条失败
[01] exit=0 0s pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' . /work | head -60 [01] exit=0 0s cat /work/AGENTS.md /work/generator/workspace/AGENTS.md /opt/tools/template_generator.py /opt/tools/template_render.mjs [02] exit=0 0s cp /opt/tools/template_generator.py generate.py [02] exit=0 0s /opt/venv/bin/python - <<'PY' [02] exit=0 0s cat /work/generator/MCIO/mcio/sketch.py [03] exit=0 0s rg -n 'class .*|def ' /work/generator/MCIO/mcio/nbt/tag.py /work/generator/MCIO/mcio/nbt/nbt.py | head -100 [04] exit=0 0s rg -n 'class LitematicaSchematic|def save|TileEntities|Entities|def to_nbt|nbt' /work/generator/MCIO/mcio/litematica.py | tail -75 [05] exit=0 0s cat > /tmp/lodge_methods.txt <<'PY' [06] exit=1 0s chmod u+w generate.py render.mjs <-- FAILED [07] exit=0 2s /opt/venv/bin/python - <<'PY' [09] exit=0 0s /opt/venv/bin/python - <<'PY' [09] exit=0 0s sed -n '1,130p' /opt/tools/cutaway.py [10] exit=1 1s /opt/venv/bin/python - <<'PY' <-- FAILED [11] exit=0 0s /opt/venv/bin/python - <<'PY' [12] exit=0 4s /opt/venv/bin/python - <<'PY' [13] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/bedroom_cutaway.litemati [13] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/attic_cutaway.litematic [13] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/living_cutaway.litematic [13] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_cutaway.litematic [15] exit=0 2s /opt/venv/bin/python - <<'PY' [17] exit=0 0s rg --files /work/generator/MCRender/resources /opt/tools | rg 'banner|pattern|1.21|assets' | head -45 [18] exit=0 0s sed -n '665,720p' /work/generator/MCRender/src/render/SpecialRenderer.ts [19] exit=0 0s /opt/venv/bin/python - <<'PY' [20] exit=0 0s /opt/venv/bin/python - <<'PY' [21] exit=0 1s /opt/venv/bin/python - <<'PY' [22] exit=0 5s /opt/venv/bin/python - <<'PY'

和有图那轮的差别


token 花在哪

归因方式与前两轮一致:看每个请求触发的命令,取最强的动作。

Fig 7. 按用途聚合的新增输入 token,每档三行(第一轮 / 修复后 / 无图)。缓存命中的输入不计入此图。
Fig 8. 按用途聚合的输出 token。

阶段对照(新增输入 token,上轮 → 这轮)

阶段lowmediumhighxhigh
探索环境与 API11,617 → 5,86919,014 → 7,83521,406 → 12,79618,097 → 19,625
写生成器2,545 → 4,2961,482 → 10,1388,789 → 25,85356,947 → 25,973
渲染看图12,069 → 7,1508,769 → 11,24013,368 → 3,5293,907 → 7,521
写测试与跑测试21,082 → 18,79817,104 → 3,72517,910 → 16,59943,907 → 6,684
修补与格式化449 → 04,819 → 2,2516,131 → 00 → 11,989
验证导出4,824 → 2,2855,012 → 7751,649 → 1,2975,988 → 0
规划与说明6,580 → 5,1045,342 → 10,8757,649 → 10,82922,739 → 9,270

逐请求细切(按时间)

low 逐请求细切 — 13 个请求
每段是一个模型请求,左起为时间顺序。hover 看命令、token 与估算金额。
medium 逐请求细切 — 19 个请求
每段是一个模型请求,左起为时间顺序。hover 看命令、token 与估算金额。
high 逐请求细切 — 20 个请求
每段是一个模型请求,左起为时间顺序。hover 看命令、token 与估算金额。
xhigh 逐请求细切 — 23 个请求
每段是一个模型请求,左起为时间顺序。hover 看命令、token 与估算金额。

花费估算

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

effort缓存输入缓存费新增输入新增输入费 输出输出费合计对比上轮
low307,840$0.03843,502$0.0549,005$0.090$0.183-58%
medium706,048$0.08846,839$0.05917,952$0.179$0.326-27%
high854,528$0.10770,903$0.08923,464$0.235$0.430-26%
xhigh1,347,456$0.16881,062$0.10136,075$0.361$0.630-31%

问题与分类

类别lowmediumhighxhigh合计
A 写文件时的语法或导入错1 → 01 → 01 → 03 → 0
B 坐标越界0 → 0
C 自写测试失败5 → 02 → 12 → 23 → 112 → 4
D shell 与工具用法错0 → 22 → 03 → 05 → 2
E 其它1 → 11 → 12 → 2
Fig 9. 失败命令按类别,三轮对照,四档相加(次)。
这一轮的逐条明细

low

  • 请求 #2:无错误关键字
    cp /opt/tools/template_generator.py generate.py
  • 请求 #3:无错误关键字
    sed -n '636,770p' /work/generator/MCIO/mcio/litematica.py; rg -n 'to_litematica|entities'

medium

  • 请求 #9:AssertionError、FAILED:test_decoration_supportstest_doors_beds_and_openingstest_envelope_exterior_flood
    mkdir -p test

high

  • 请求 #7:AssertionError
    chmod u+w generate.py render.mjs
  • 请求 #11:AssertionError、FAILED:test_all_rooms_reachable_with_two_block_headroomtest_banner_item_frame_lectern_and_forge_nbttest_roof_junction_and_hand_built_pinestest_window_and_door_openings_clear
    /opt/venv/bin/python - <<'PY'
  • 请求 #12:AssertionError、FAILED:test_window_and_door_openings_clear
    /opt/venv/bin/python - <<'PY'

xhigh

  • 请求 #6:AssertionError
    chmod u+w generate.py render.mjs
  • 请求 #10:AssertionError、FAILED:test_windows_keep_glazing_and_open_sightlines
    /opt/venv/bin/python - <<'PY'

测试对比

effort第一轮修复后无图破坏式断言行数(上轮 → 这轮)
low8 个 / 133 行9 个 / 174 行7 个 / 108 行1 → 1
medium9 个 / 86 行7 个 / 165 行7 个 / 166 行2 → 2
high12 个 / 154 行10 个 / 207 行11 个 / 211 行3 → 2
xhigh12 个 / 127 行14 个 / 236 行12 个 / 208 行5 → 5

类别覆盖(测试体行数,上轮 → 这轮)

类别lowmediumhighxhigh
导出回读7 → 0 行12 → 0 行19 → 0 行0 → 9 行
状态与尺寸合法性10 → 8 行0 → 12 行10 → 14 行16 → 15 行
确定性8 → 9 行7 → 6 行11 → 8 行10 → 0 行
门床配对23 → 16 行21 → 34 行27 → 40 行33 → 28 行
可达性42 → 27 行32 → 31 行61 → 41 行22 → 14 行
封闭性43 → 14 行44 → 28 行31 → 32 行54 → 0 行
支撑与悬空28 → 22 行28 → 21 行0 → 11 行44 → 35 行
窗洞13 → 12 行8 → 0 行10 → 10 行
照明21 → 34 行17 → 19 行17 → 13 行
实体与 NBT23 → 28 行30 → 30 行
其它0 → 18 行0 → 54 行
Fig 10. 按类别统计的测试体行数,四档相加,三轮对照。一个测试只归一个类别,按函数名的首要关键词判定。

四份代码

effort生成器行数(上轮 → 这轮)函数个数与上轮同名的函数其它 .py 文件
low575 → 43213 → 1612
medium536 → 48421 → 2017
high754 → 54941 → 2215preview_slices.py 37 行
xhigh899 → 71843 → 2616make_previews.py 37 行

同名函数矩阵

这一轮四档之间

lowmediumhighxhigh
low16
自身
141314
medium1420
自身
1415
high131422
自身
16
xhigh14151626
自身

跨轮:行是上一轮,列是这一轮

lowmediumhighxhigh
low12121212
medium14171517
high14181518
xhigh14171516

完整源码

low medium high xhigh
generate.py — 432 行
# 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, Int, String, Byte, Double, 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": (45, 40, 35), # width X, height Y, depth Z "minecraft_version": "1.21.1", "data_version": 3955, "output": "output.litematic", "name": "Amberstone Lodge", "author": "generator", "description": "Amberstone Lodge: four furnished levels, smithy, terrace and pines", } def canonical(state): """Normalize property ordering for lossless round-trip comparisons.""" base, _, props = state.partition('[') return base, tuple(sorted(props.rstrip(']').split(','))) 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, **properties) 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 lodge, landscape, circulation and furnished room groups.""" self.decorations = [] self.windows = [] self.tile_entities = [] self.entities = [] stone = ['minecraft:stone_bricks'] * 6 + ['minecraft:andesite', 'minecraft:mossy_stone_bricks'] plaster = ['minecraft:calcite'] * 7 + ['minecraft:smooth_sandstone', 'minecraft:white_terracotta'] roof = ['minecraft:deepslate_tiles'] * 7 + ['minecraft:deepslate_bricks', 'minecraft:polished_deepslate'] wood = ['minecraft:spruce_planks'] * 8 + ['minecraft:dark_oak_planks'] self.box((0, 0, 0, 44, 1, 34), 'minecraft:dirt') self.box((0, 2, 0, 44, 2, 34), 'minecraft:grass_block') self.texture((6, 2, 5, 38, 3, 30), stone) for z in range(5): center = 18 + (z // 2) for x in range(center - 2, center + 3): self.put(x, 2, z, self.rng.choice(stone + ['minecraft:gravel'])) for x in range(16, 22): self.put(x, 3, 4, 'minecraft:stone_brick_stairs', facing='south') # Main and lower wing shells; shared walls are opened after roofs are built. for x0, x1, z0, z1, top, floors in [(8, 24, 10, 28, 25, [3, 10, 17, 24]), (24, 36, 12, 27, 17, [3, 10])]: for y in range(4, top + 1): pal = stone if y < 10 else plaster for x in range(x0, x1 + 1): for z in (z0, z1): self.put(x, y, z, self.rng.choice(pal)) for z in range(z0, z1 + 1): for x in (x0, x1): self.put(x, y, z, self.rng.choice(pal)) for f in floors: self.texture((x0, f, z0, x1, f, z1), wood) for x in range(x0, x1 + 1, 4): for z in (z0, z1): self.box((x, 4, z, x, top, z), 'minecraft:stripped_spruce_log', axis='y') for z in range(z0, z1 + 1, 4): for x in (x0, x1): self.box((x, 4, z, x, top, z), 'minecraft:stripped_dark_oak_log', axis='y') for y in floors[1:] + [top]: for z in (z0, z1): self.box((x0, y, z, x1, y, z), 'minecraft:dark_oak_log', axis='x') for x in (x0, x1): self.box((x, y, z0, x, y, z1), 'minecraft:dark_oak_log', axis='z') # Steep filled gables and two-block thick roofs eliminate diagonal sky leaks. for x0, x1, z0, z1, eave, rise in [(7, 25, 9, 29, 25, 12), (24, 37, 11, 28, 17, 7)]: mid = (x0 + x1) / 2 heights = {x: eave + int(rise * (1 - abs(x - mid) / ((x1 - x0) / 2))) for x in range(x0, x1 + 1)} for x, h in heights.items(): for z in range(z0, z1 + 1): # Lower roof terminates against main wall. if x <= 24 and eave == 17: continue low = min(h, heights.get(x - 1, h), heights.get(x + 1, h)) - 1 self.texture((x, low, z, x, h, z), roof) if x0 < x < x1: for z in (z0 + 1, z1 - 1): if eave == 17 and x <= 24: continue self.texture((x, eave, z, x, h - 2, z), plaster) if x % 4 == 0: self.box((x, eave, z, x, h - 2, z), 'minecraft:dark_oak_log', axis='y') for z in (z0, z1): self.put(x, h, z, 'minecraft:polished_deepslate') peak = max(heights.values()) for z in range(z0, z1 + 1): self.put(round(mid), peak + 1, z, 'minecraft:deepslate_tile_slab', type='bottom') # Clear internal doorways between the masses. for f in (3, 10): self.box((24, f + 1, 19, 24, f + 3, 21), 'minecraft:air') # Windows: full glass keeps the weather envelope sealed. for y in (5, 12, 19, 27): for x in (12, 20): if y == 27 and x == 20: continue self.window(x, y, 10, 'north') if y < 25: for x in (12, 20): self.window(x, y, 28, 'south') for y in (5, 12): self.window(30, y, 12, 'north') self.window(30, y, 27, 'south') self.window(36, y, 16, 'east') for y in (5, 12, 19): self.window(8, y, 18, 'west') for y, half in ((4, 'lower'), (5, 'upper')): self.put(16, y, 10, 'minecraft:spruce_door', half=half, facing='north', hinge='left', open='false', powered='false') self.box((15, 6, 9, 17, 6, 9), 'minecraft:spruce_log', axis='x') for x in (14, 18): self.put(x, 5, 9, 'minecraft:spruce_fence') self.put(x, 6, 9, 'minecraft:lantern', hanging='false') self.put(x, 4, 9, 'minecraft:stone_bricks') # Backed ladder shaft, with two clear blocks above every landing. self.box((11, 4, 27, 11, 28, 27), 'minecraft:spruce_planks') for y in range(4, 27): self.put(11, y, 26, 'minecraft:ladder', facing='north') # Furnish each room with an intentionally clear central circulation aisle. for floor in (3, 10, 17, 24): self.room(12 if floor == 24 else 9, floor, 13 if floor == 24 else 12) for floor in (3, 10): self.room(26, floor, 14) # Living and dining furniture. for x in range(16, 21): self.put(x, 11, 23, 'minecraft:spruce_stairs', facing='south') self.box((16, 11, 18, 19, 11, 19), 'minecraft:spruce_planks') for x in (16, 19): self.put(x, 12, 18, 'minecraft:yellow_carpet') self.put(17, 12, 19, 'minecraft:candle', candles='3', lit='true') # Bedroom dais, complete green bed, cabinets and patterned curtains. self.box((16, 18, 22, 21, 18, 25), 'minecraft:spruce_planks') for x in range(16, 22): self.put(x, 18, 21, 'minecraft:spruce_stairs', facing='south') self.put(18, 19, 23, 'minecraft:green_bed', facing='south', part='foot', occupied='false') self.put(18, 19, 24, 'minecraft:green_bed', facing='south', part='head', occupied='false') for x in (16, 21): self.put(x, 19, 24, 'minecraft:barrel', facing='up') self.put(x, 20, 24, 'minecraft:lantern', hanging='false') for x in (11, 14): self.put(x, 21, 11, 'minecraft:green_wall_banner', facing='south') self.tile_entities.append(Compound({'id': String('minecraft:banner'), 'x': Int(x), 'y': Int(21), 'z': Int(11), 'patterns': NBTList[Compound]([Compound({'pattern': String('minecraft:stripe_center'), 'color': String('yellow')}), Compound({'pattern': String('minecraft:stripe_bottom'), 'color': String('white')})])})) for x in (21, 22): self.put(x, 21, 27, 'minecraft:barrel', facing='north') self.box((21, 25, 20, 22, 27, 20), 'minecraft:bookshelf') self.put(18, 25, 20, 'minecraft:lectern', facing='north', has_book='false') # Blacksmith canopy and lit forge with solid chimney. for x in (26, 36): for z in (6, 10): self.box((x, 4, z, x, 8, z), 'minecraft:spruce_log', axis='y') self.put(x, 4, z, 'minecraft:stone_bricks') for z in range(5, 12): self.texture((25, 9 + (z - 5) // 3, z, 37, 9 + (z - 5) // 3, z), roof) self.texture((32, 4, 9, 34, 5, 11), stone) self.put(33, 4, 8, 'minecraft:stone_bricks') self.put(33, 5, 8, 'minecraft:blast_furnace', facing='north', lit='true') self.put(33, 5, 9, 'minecraft:magma_block') self.put(33, 6, 9, 'minecraft:campfire', lit='true', signal_fire='false', waterlogged='false', facing='north') self.texture((32, 7, 10, 34, 24, 11), stone) self.box((32, 25, 10, 34, 25, 11), 'minecraft:stone_brick_slab', type='bottom') self.put(29, 4, 8, 'minecraft:anvil', facing='east') self.put(27, 4, 10, 'minecraft:barrel', facing='north') self.put(28, 4, 10, 'minecraft:chest', facing='north', type='single') self.put(27, 5, 10, 'minecraft:lantern', hanging='false') # Terrace balustrades and stonework vines. for x in list(range(6, 14)) + list(range(23, 39)): if x in (26, 36): continue self.put(x, 4, 5, 'minecraft:stone_brick_wall') for y in range(4, 9): for z in range(21, 25): if self.rng.random() < .75: self.put(7, y, z, 'minecraft:vine', east='true') self.pine(3, 23, 15) self.pine(40, 28, 19) self.pine(40, 7, 12) for x, z in ((4, 7), (5, 13), (40, 17), (30, 32), (13, 32), (3, 30)): self.put(x, 3, z, 'minecraft:fern') return self def window(self, x, y, z, facing): """Insert framed sealed glazing and shutters on a facade.""" dx, dz = (1, 0) if facing in ('north', 'south') else (0, 1) ox, oz = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}[facing] for a in range(2): for b in range(3): p = (x + dx * a, y + b, z + dz * a) self.put(*p, 'minecraft:glass') self.windows.append(p) for a in (-1, 2): for b in range(3): self.put(x + dx * a, y + b, z + dz * a, 'minecraft:dark_oak_log', axis='y') self.put(x + dx * a + ox, y + b, z + dz * a + oz, 'minecraft:spruce_trapdoor', facing=facing, open='true', half='bottom') for a in range(-1, 3): self.put(x + dx * a, y - 1, z + dz * a, 'minecraft:spruce_planks') self.put(x + dx * a, y + 3, z + dz * a, 'minecraft:dark_oak_log', axis='x' if dx else 'z') def room(self, x, f, z): """Add storage, seats, textiles, light and wall display to one room.""" self.put(x, f + 1, z, 'minecraft:barrel', facing='south') self.put(x + 1, f + 1, z, 'minecraft:chest', facing='south', type='single') self.put(x, f + 2, z, 'minecraft:lantern', hanging='false') self.put(x + 3, f + 1, z, 'minecraft:bookshelf') self.put(x + 3, f + 2, z, 'minecraft:potted_spruce_sapling') self.put(x + 4, f + 1, z, 'minecraft:spruce_planks') self.put(x + 4, f + 2, z, 'minecraft:candle', candles='2', lit='true') self.put(x + 3, f + 1, z + 3, 'minecraft:spruce_stairs', facing='north') self.box((x + 4, f + 1, z + 3, x + 6, f + 1, z + 5), 'minecraft:green_carpet') self.put(x + 6, f + 1, z, 'minecraft:crafting_table') # Small backed utility ladder in each room also provides hanging tool storage. self.put(x + 6, f + 2, z, 'minecraft:spruce_planks') self.put(x + 6, f + 2, z + 1, 'minecraft:ladder', facing='south') self.entities.append(Compound({'id': String('minecraft:item_frame'), 'Pos': NBTList[Double]([Double(x + 3.5), Double(f + 1.5), Double(z + 1.03125)]), 'Facing': Byte(3), 'TileX': Int(x + 3), 'TileY': Int(f + 1), 'TileZ': Int(z + 1), 'Item': Compound({'id': String('minecraft:iron_axe'), 'count': Int(1)}), 'ItemRotation': Byte(0)})) def pine(self, x, z, height): """Place a trunk, stepped log boughs and layered persistent needles.""" self.box((x, 3, z, x, height + 3, z), 'minecraft:spruce_log', axis='y') for y in range(6, height + 3, 3): radius = max(1, 4 - (y - 6) // 4) for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dz) > radius + 1 or (dx == 0 and dz == 0): continue if self.inside(x + dx, y, z + dz): self.put(x + dx, y, z + dz, 'minecraft:spruce_leaves', persistent='true', distance='1') if abs(dx) + abs(dz) <= radius: self.put(x + dx, y + 1, z + dz, 'minecraft:spruce_leaves', persistent='true', distance='1') for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)): self.put(x + dx, y, z + dz, 'minecraft:spruce_log', axis='x' if dx else 'z') for y in range(height + 1, height + 4): for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)): self.put(x + dx, y, z + dz, 'minecraft:spruce_leaves', persistent='true', distance='1') self.put(x, height + 4, z, 'minecraft:spruce_leaves', persistent='true', distance='1') 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"] = NBTList[Compound](self.tile_entities) region["Entities"] = NBTList[Compound](self.entities) 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 canonical(loaded.palette[int(ids[y, z, x])]) == canonical(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()
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: 210, elevation: 18, radius: 3.3 }, { name: 'rear', azimuth: 35, elevation: 22, radius: 3.3 }, { name: 'side', azimuth: 120, elevation: 14, radius: 3.3 }, { name: 'top', azimuth: 210, elevation: 62, radius: 3.4 }, ]; // Resolve every path while the working directory is still the caller's. const absoluteInput = path.resolve(INPUT); const absoluteOutDir = path.resolve(OUT_DIR); fs.mkdirSync(absoluteOutDir, { recursive: true }); // The renderer looks for its resource cache relative to the working directory, so move there first, // and only then create the scene. process.chdir(RENDER_ROOT); const scene = createRenderScene({ width: SIZE, height: SIZE }); for (const view of VIEWS) { const result = renderLitematicWithMetadata(absoluteInput, { width: SIZE, height: SIZE, fov_deg: 30, radius: view.radius, nRandomAzimuths: 0, azimuthInit: 0, elevationInit: 0, azimuthsDeltas: [view.azimuth], elevationsDeltas: [view.elevation], ambientIntensities: [AMBIENT], lightDirections: [LIGHT_DIRECTION], mirror: [false], layout: [[0]], limit: 512, seed: 0, opaque: false, }, scene); // Flatten onto an opaque background so shaded faces keep their contrast. for (let i = 0; i < result.png.data.length; i += 4) { const alpha = result.png.data[i + 3] / 255; for (let channel = 0; channel < 3; channel++) { result.png.data[i + channel] = Math.round(result.png.data[i + channel] * alpha + BACKGROUND[channel] * (1 - alpha)); } result.png.data[i + 3] = 255; } const target = path.join(absoluteOutDir, `${view.name}.png`); fs.writeFileSync(target, PNG.sync.write(result.png)); console.log(`wrote ${target} (${SIZE}x${SIZE}, azimuth ${view.azimuth}, elevation ${view.elevation})`); }
test/test_structure.py — 149 行
# Structural checks against the exported schematic and its entity data. import sys from pathlib import Path from collections import deque import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from generate import Build, canonical from mcio.schematic import load_schematic from mcio.nbt.nbt import load ROOT = Path(__file__).resolve().parents[1] @pytest.fixture(scope='module') def data(): s = load_schematic(ROOT / 'output.litematic') ids = s.read_flat(0, s.volume).reshape(s.size_yzx) return {(x, y, z): s.palette[int(ids[y, z, x])] for y in range(40) for z in range(35) for x in range(45)} def base(s): return s.split('[')[0].split(':')[-1] def props(s): return dict(v.split('=') for v in s.partition('[')[2].rstrip(']').split(',') if '=' in v) def test_roundtrip_and_determinism(tmp_path, data): b = Build().build() b.verify(ROOT / 'output.litematic') a = b.export(tmp_path / 'a.litematic') c = Build().build().export(tmp_path / 'b.litematic') assert a.read_bytes() == c.read_bytes() == (ROOT / 'output.litematic').read_bytes() n = load(a) assert int(n['MinecraftDataVersion']) == 3955 assert len(n['Regions']) == 1 def test_doors_beds(data): for (x, y, z), s in data.items(): p = props(s) if base(s).endswith('_door'): other = data[x, y + (1 if p['half'] == 'lower' else -1), z] q = props(other) assert base(other) == base(s) assert q['half'] != p['half'] assert (q['facing'], q['hinge']) == (p['facing'], p['hinge']) if base(s).endswith('_bed'): dx, dz = {'south': (0, 1), 'north': (0, -1), 'east': (1, 0), 'west': (-1, 0)}[p['facing']] sign = 1 if p['part'] == 'foot' else -1 other = data[x + dx * sign, y, z + dz * sign] assert base(other) == base(s) assert props(other)['facing'] == p['facing'] assert props(other)['part'] != p['part'] def test_weather_envelope(data): # Flood exterior air from every boundary; glazing and closed doors seal the shell. air = {p for p, s in data.items() if base(s) == 'air'} seen = {p for p in air if p[0] in (0, 44) or p[1] in (0, 39) or p[2] in (0, 34)} queue = deque(seen) 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 q in air and q not in seen: seen.add(q) queue.append(q) leaked = [p for p in seen if (9 <= p[0] <= 23 and 11 <= p[2] <= 27 and 4 <= p[1] <= 25) or (25 <= p[0] <= 35 and 13 <= p[2] <= 26 and 4 <= p[1] <= 16)] assert not leaked, leaked[:15] def test_reachable_rooms_with_headroom(data): def clear(p): s = base(data.get(p, 'minecraft:stone')) return s in ('air', 'ladder') or s.endswith('_carpet') or s.endswith('_door') def stand(p): x, y, z = p return clear(p) and clear((x, y + 1, z)) and (not clear((x, y - 1, z)) or base(data.get(p, '')) == 'ladder') start = (16, 4, 9) seen = {start} queue = deque(seen) while queue: x, y, z = queue.popleft() candidates = [(x + dx, y + dy, z + dz) for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)) for dy in (-1, 0, 1)] if base(data[x, y, z]) == 'ladder': candidates += [(x, y + 1, z), (x, y - 1, z)] for q in candidates: if q not in seen and stand(q): # Steps need enough head clearance over their higher floor. if q[1] > y and not clear((x, y + 2, z)): continue seen.add(q) queue.append(q) for p in [(16, 4, 15), (16, 11, 15), (16, 18, 15), (16, 25, 18), (30, 4, 21), (30, 11, 21), (18, 19, 22)]: assert p in seen, p def test_windows_and_decorations(data): b = Build().build() for p in b.windows: assert base(data[p]) == 'glass', p for (x, y, z), s in data.items(): name = base(s) if name in ('lantern', 'candle', 'potted_spruce_sapling') or name.endswith('_carpet') or name.endswith('_bed'): assert base(data[x, y - 1, z]) not in ('air', 'ladder', 'vine'), (x, y, z, name) if name == 'ladder' or name.endswith('_wall_banner'): dx, dz = {'north': (0, 1), 'south': (0, -1), 'east': (-1, 0), 'west': (1, 0)}[props(s)['facing']] assert base(data[x + dx, y, z + dz]) not in ('air', 'ladder'), (x, y, z) for f, x, z in [(3, 9, 12), (10, 9, 12), (17, 9, 12), (24, 12, 13), (3, 26, 14), (10, 26, 14)]: assert base(data[x, f + 2, z]) == 'lantern' region = next(iter(load(ROOT / 'output.litematic')['Regions'].values())) assert len(region['Entities']) == 6 for e in region['Entities']: x, y, z = (int(e[k]) for k in ('TileX', 'TileY', 'TileZ')) assert base(data[x, y, z - 1]) == 'bookshelf' assert str(e['Item']['id']) == 'minecraft:iron_axe' assert len(region['TileEntities']) == 2 for e in region['TileEntities']: assert len(e['patterns']) == 2 def test_valid_vanilla_states(data): import json schema = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) for s in set(data.values()): assert base(s) in schema allowed = schema[base(s)][0] for k, v in props(s).items(): assert k in allowed and v in allowed[k], s def test_forge_and_openings(data): assert props(data[33, 5, 8])['lit'] == 'true' assert props(data[33, 6, 9])['lit'] == 'true' assert base(data[33, 4, 8]) == 'stone_bricks' assert base(data[29, 4, 8]) == 'anvil' for y in (4, 5): for z in (9, 11): assert base(data[16, y, z]) == 'air' for f in (3, 10): for y in (f + 1, f + 2): for x in (23, 24, 25): assert base(data[x, y, 20]) == 'air'

解题方式

三轮都一样的

这一轮的不同


harness 改进点

  1. 把材质多样性写进 prompt 的可检查条款。这一轮掉的就是它,而且没有任何测试覆盖。 比如"每个外墙面至少用三种同族方块,且没有单一方块占比超过七成",可以直接做成打分器指标。
  2. NBT 类断言仍然是失败主因。与上一轮结论相同:旗帜图案、书、展示框、容器物品。 把这些的读写与断言做成模板函数。
  3. 把负控制写回环境说明。破坏式断言仍停在 1/2/2/5,和上轮一样偏少。
  4. 渲染张数给个建议区间。有图时 low 渲了 20 张,无图时只有 6 张,两头都不稳。
  5. 文字规格可以再具体一点。这一轮证明模型完全能照文字建,那么把"参考图里那种感觉"翻译成 可量化条款(层高、开窗率、材质配比),就能在不给图的情况下拿回细节密度。

前两轮的报告:第一轮修复后


数据来自 runs/archive/lodge-v{1,2,3}-*out/report.jsonout/session/raw_session.jsonl。第一轮的耗时与渲染张数取自第一份报告,因为那批 run 的 report.json 在一次误删中丢失,产物指标则是从归档的 schematic 重新测出来的。