Contents

harness 修复前后对比


同一个任务、同一个模型、同样四档 effort,跑两轮。 第二轮的唯一变化是 harness:API 速查表、生成器模板、渲染模板、渲染器能力说明、取消写报告要求。

-53%探索阶段输入 token
-7%四档合计费用
4/4产物字节可复现(上轮 3/4)
8/8两轮全部通过

结论

  1. 整体 dollar amount 不变, 但质量明显提升很多。质量正比于测试行数。
  2. 质量在 high/xhigh 饱和, low/medium 追赶修复前质量。
  3. 渲染抓的是比例, 材质, 檐口穿插。测试抓的是可达性, 封闭性, 悬空。对于质量来说测试更重要。
  4. 修复后同名函数变多了, 可以理解为低 effort 加了本该由高 effort 写的代码。
  5. 现有 bug: 旗帜图案, 书, 展示框, 容器物品是大半失败的原因。 明确写不要加 entity(如展示框),明确 block entity (Block entities can store additional information about their block, beyond the finite set of block states that a block may have.) 只加 block states 不加 block entity state。 可以提供一个 validate_state(name, **props) 帮助函数。
$2.54 → $2.37四档合计估算
500 → 782测试体行数
7 → 13四档共有的同名函数
6 / 22失败命令涉及 NBT 断言

改了什么

  1. API 速查表进 promptLitematicCanvas 索引、material 需要命名空间、 from mcio.nbt.nbt import File as NBTFile 等,之前每档要花 4–10 条命令读源码才知道。
  2. 生成器模板 /opt/tools/template_generator.py:内置带边界断言的 put(x, y, z)box/shell/texture、单一 seeded 随机源、时间戳清零的确定性导出、回读校验。 prompt 里因此不再解释这些。
  3. 渲染模板 /opt/tools/template_render.mjs(四个调好的视角)与 render_schematic.mjs(单视角 CLI)、cutaway.py(切片看内部)。 光照参数已调好,之前每档都要翻渲染器源码找 lightDirection
  4. 渲染器能力清单:旗帜图案、展示框、讲台书、block entity NBT 在预览里不显示,改用 NBT 断言验证。
  5. 取消写 REPORT.md:改为直接看 trace。
  6. 措辞只写要做什么,去掉所有 "do not" 式否定句。

我们给 agent 的全部东西

这一轮 agent 手上多出来的就是下面六份文件。除此之外容器没变:同样的库栈、同样的只读挂载、同样的打分器。

文件行数作用
task/BRIEF.md72任务本身。这份文件的全文就是发给模型的第一轮也是唯一一轮 user turn。换任务只换它。
ENVIRONMENT.md113环境说明,和任务无关,跟镜像同生命周期。拼在任务后面一起发出,同时拷进工作区。
tools/template_generator.py220生成器模板。带边界断言的 put、box/shell/texture、单一 seeded 随机源、时间戳清零的确定性导出、回读校验。agent 拷走填 build()。
tools/template_render.mjs73渲染模板,四个调好的视角。光照参数写死在顶部并注明理由。
tools/render_schematic.mjs77单视角渲染 CLI,参数化相机、尺寸、背景、环境光。
tools/cutaway.py107切片工具,沿 X/Y/Z 任一轴切开建筑,用来渲染内部。

前两份进 prompt(任务 + 环境说明,拼起来 188 行),后四份放在镜像的 /opt/tools, 只读,agent 想改就拷进工作区改。

task/BRIEF.md — 72 行

任务本身。这份文件的全文就是发给模型的第一轮也是唯一一轮 user turn。换任务只换它。

# 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 `ENVIRONMENT.md`, which is appended to this prompt and also sits in your working directory.
ENVIRONMENT.md — 113 行

环境说明,和任务无关,跟镜像同生命周期。拼在任务后面一起发出,同时拷进工作区。

# Environment You are generating Minecraft content in a container. This document applies to every task run here; the task itself is stated separately. A copy of this file sits in your working directory as `ENVIRONMENT.md`, and the original is at `/opt/ENVIRONMENT.md`. ## Start from the templates Two templates are installed and tested. Copy them into your workspace and edit them. ```sh cp /opt/tools/template_generator.py generate.py # then fill in Build.build() cp /opt/tools/template_render.mjs render.mjs # then edit VIEWS ``` `template_generator.py` already provides everything the harness checks: - `Build.put(x, y, z, block, **properties)` places one block in XYZ order and asserts the coordinate is inside the volume, naming the coordinate when it is outside. - `Build.box(bounds, block)` fills an inclusive box, `Build.shell(bounds, block)` fills only the faces, `Build.texture(bounds, palette)` mixes related blocks so a surface reads as varied stone or plaster. - `Build.get(x, y, z)` reads back what was placed. - `CONFIG["seed"]` feeds a single `random.Random` on the instance as `self.rng`. Use `self.rng` for every random decision, and the build stays reproducible. - `Build.export(path)` writes the schematic with the metadata timestamps zeroed, so two runs produce identical bytes. - `Build.verify(path)` reloads the file and asserts every placed cell survived the round trip. Run it: `/opt/venv/bin/python generate.py` writes `output.litematic` and prints the reload report. ## Rendering Rendering runs inside this container on a software GL stack. A 512 pixel view takes about 1.5 seconds. ```sh # four tuned views of a build, written into previews/ xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/template_render.mjs -- \ output.litematic previews # one view with explicit framing xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- \ --input output.litematic --output previews/front.png \ --size 512 --azimuth 210 --elevation 18 --radius 3.3 # see inside: slice first, then render the slice /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/slice.litematic --max-y 12 /opt/venv/bin/python /opt/tools/cutaway.py --input output.litematic --output previews/half.litematic --axis x --max 20 ``` `render_schematic.mjs` flags: `--input --output --size --azimuth --elevation --radius --fov --ambient --background r,g,b,a`. The ambient level, light direction and background in both scripts are already tuned so roof, wall and ground planes separate cleanly. ## What the renderer draws, and what to assert instead The renderer draws block geometry and textures. These parts of a build are verified by reloading the schematic and asserting on its data: - banner patterns, which render as a plain banner - item frames and their contents - lectern books - block entity NBT in general, including container inventories - anything dropped by a cutaway export, which keeps block geometry only Minecraft's own shader lighting, smoke particles and sky are outside this renderer, so judge lighting design by where you placed light sources rather than by picture brightness. ## Library reference The templates cover the common path. Reach for these when you go beyond them. ```python import sys sys.path.insert(0, "/work/generator/MCIO") # mcio is imported from source from mcio.sketch import LitematicCanvas, Material, parse_blockstate from mcio.schematic import load_schematic from mcio.nbt.nbt import File as NBTFile from mcio.nbt.tag import Compound, Int, String, List as NBTList canvas = LitematicCanvas((size_y, size_z, size_x)) # the canvas itself is indexed (y, z, x) canvas.block((y, z, x), "minecraft:stone_bricks") canvas.block((y, z, x), canvas.material("minecraft:oak_stairs", facing="north", half="bottom", shape="straight")) canvas.block((y, z, x), parse_blockstate("minecraft:oak_door[half=upper,facing=north,hinge=left]")) canvas.blocks(positions, "minecraft:glass") loaded = load_schematic(path) size_y, size_z, size_x = loaded.size_yzx ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) state_string = loaded.palette[int(ids[y, z, x])] ``` Facts worth keeping in mind: - Block names carry the `minecraft:` namespace, in `block()`, `material()` and `parse_blockstate()` alike. - `y` is height, `x` is width, `z` is depth. North is `-z`, south is `+z`, west is `-x`, east is `+x`. The template's `put()` takes `(x, y, z)`; the raw canvas takes `(y, z, x)`. - `/work/generator/MCRender/resources/1.21.1-blocks.json` is the authority on which block states and property values exist. Load it to validate a state you are unsure about. ## Tooling - `/opt/venv/bin/python`: python 3.12 with numpy, deprecated, tqdm, pytest, yapf. First on `PATH`. - `node` 24, `uv`, `git`, `ripgrep`, `zsh`, `xvfb-run`. - `/work/generator/MCIO` and `/work/generator/MCRender`: read only library sources, safe to read for reference. - Repo conventions in `/work/AGENTS.md`, yapf style in `/work/.style.yapf`. ## Limits - `/work` and `/tmp` are writable, which covers the workspace, previews and any scratch files. - The model API is reachable. Everything else you need is already inside the container. - The container has no GPU, and the software renderer handles these sizes comfortably. - When you finish, state plainly in your final message what is done and what is still unfinished.
tools/template_generator.py — 220 行

生成器模板。带边界断言的 put、box/shell/texture、单一 seeded 随机源、时间戳清零的确定性导出、回读校验。agent 拷走填 build()。

# 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.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": "build", "author": "generator", "description": "Generated build", } 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": """Place the blocks of the build. Replace this body with the actual design. Returns: Build: This instance, so calls can be chained. """ size_x, size_y, size_z = self.size_x, self.size_y, self.size_z self.box((0, 0, 0, size_x - 1, 0, size_z - 1), "minecraft:grass_block") self.texture((4, 1, 4, size_x - 5, 1, size_z - 5), ["minecraft:stone_bricks"] * 6 + ["minecraft:cobblestone"] * 3 + ["minecraft:mossy_cobblestone"]) 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()
tools/template_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})`); }
tools/render_schematic.mjs — 77 行

单视角渲染 CLI,参数化相机、尺寸、背景、环境光。

// Render any .litematic to a PNG with the bundled headless renderer. // Usage: xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- \ // --input build.litematic --output preview.png [--size 512] [--azimuth 210] [--elevation 20] // [--radius 3.2] [--fov 30] [--background 47,52,59,255] [--ambient 0.7] 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'); const args = process.argv.slice(2).filter(entry => entry !== '--'); const flag = (name, fallback) => { const index = args.indexOf(`--${name}`); return index >= 0 && args[index + 1] !== undefined ? args[index + 1] : fallback; }; const input = flag('input'); if (!input) { console.error('usage: --input <file.litematic> [--output <file.png>] [--size N] [--azimuth D] [--elevation D] [--radius R] [--fov D] [--background r,g,b,a] [--ambient A]'); process.exit(2); } const output = flag('output', input.replace(/\.litematic$/, '') + '.png'); const size = Number(flag('size', 512)); const azimuth = Number(flag('azimuth', 210)); const elevation = Number(flag('elevation', 20)); const radius = Number(flag('radius', 3.2)); const fov = Number(flag('fov', 30)); const ambient = Number(flag('ambient', 0.7)); const background = flag('background', '47,52,59,255').split(',').map(Number); const absoluteInput = path.resolve(input); const absoluteOutput = path.resolve(output); if (!fs.existsSync(absoluteInput)) { console.error(`no such schematic: ${absoluteInput}`); process.exit(2); } // The resource cache resolves relative to the renderer root, so run from there. process.chdir(RENDER_ROOT); const scene = createRenderScene({ width: size, height: size }); const result = renderLitematicWithMetadata(absoluteInput, { width: size, height: size, fov_deg: fov, radius: radius, nRandomAzimuths: 0, azimuthInit: 0, elevationInit: 0, azimuthsDeltas: [azimuth], elevationsDeltas: [elevation], ambientIntensities: [ambient], lightDirections: [[0.08, 0.28, 0.15]], mirror: [false], layout: [[0]], limit: 512, seed: 0, opaque: false, }, scene); // Flatten onto the requested background so transparent sky does not read as white. if (background.length === 4) { 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; } } fs.mkdirSync(path.dirname(absoluteOutput), { recursive: true }); fs.writeFileSync(absoluteOutput, PNG.sync.write(result.png)); fs.writeFileSync(absoluteOutput.replace(/\.png$/, '') + '.camera.json', JSON.stringify(result.camera, null, 2) + '\n'); console.log(`wrote ${absoluteOutput} (${size}x${size}, azimuth ${azimuth}, elevation ${elevation})`);
tools/cutaway.py — 107 行

切片工具,沿 X/Y/Z 任一轴切开建筑,用来渲染内部。

# Export a sliced copy of a schematic so the inside can be rendered from outside. # Works on any build: cut below a height, or slice along X or Z to get a cross section. # Usage: /opt/venv/bin/python /opt/tools/cutaway.py --input build.litematic --output slice.litematic --max-y 12 # /opt/venv/bin/python /opt/tools/cutaway.py --input build.litematic --output half.litematic --axis x --max 20 from __future__ import annotations import argparse import gzip import sys from pathlib import Path from typing import Optional sys.path.insert(0, "/work/generator/MCIO") from mcio.nbt.nbt import File as NBTFile # noqa: E402 from mcio.schematic import load_schematic # noqa: E402 from mcio.sketch import LitematicCanvas, parse_blockstate # noqa: E402 AIR = {"minecraft:air", "minecraft:cave_air", "minecraft:void_air"} FALLBACK_DATA_VERSION = 3955 # Java 1.21.1, the version shipped in this image def source_data_version(path: Path) -> int: """Read the Minecraft data version of a schematic, falling back to the shipped version. Args: path (Path): Schematic to inspect. Returns: int: Data version stored in the file, or the shipped default. """ try: with gzip.open(path, "rb") as stream: root = NBTFile.from_buffer(stream).root return int(root["MinecraftDataVersion"]) except Exception: return FALLBACK_DATA_VERSION def slice_schematic(source: Path, destination: Path, axis: str, limit: int, keep_above: bool, name: str) -> None: """Copy a schematic while dropping everything beyond a cut plane. Args: source (Path): Schematic to read. destination (Path): Schematic to write. axis (str): Axis to cut along, one of x, y, z. limit (int): Last index to keep along that axis, inclusive. keep_above (bool): Keep the far side of the cut instead of the near side. name (str): Region name for the copy. """ loaded = load_schematic(source) size_y, size_z, size_x = loaded.size_yzx ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) axis_size = {"y": size_y, "z": size_z, "x": size_x}[axis] cut = max(0, min(limit, axis_size - 1)) lower, upper = (cut + 1, axis_size - 1) if keep_above else (0, cut) span = upper - lower + 1 sizes = {"y": size_y, "z": size_z, "x": size_x} sizes[axis] = span canvas = LitematicCanvas((sizes["y"], sizes["z"], sizes["x"])) for y in range(size_y): for z in range(size_z): for x in range(size_x): index = {"y": y, "z": z, "x": x}[axis] if not lower <= index <= upper: continue state = loaded.palette[int(ids[y, z, x])] if state.split("[")[0] in AIR: continue target = {"y": y, "z": z, "x": x} target[axis] = index - lower canvas.block((target["y"], target["z"], target["x"]), parse_blockstate(state)) schematic = canvas.to_litematica(name=name, author="cutaway", description=f"{source.name} {axis}<={cut}", minecraft_data_version=source_data_version(source)) # Deterministic output: the schematic metadata otherwise records the current wall clock. schematic.metadata.time_created = 0 schematic.metadata.time_modified = 0 schematic.save(destination) def main(argv: Optional[list] = None) -> None: """Run one slice export. Args: argv (Optional[list]): Command line arguments, defaulting to sys.argv. """ parser = argparse.ArgumentParser(description="Slice a schematic so its interior can be rendered.") parser.add_argument("--input", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--axis", choices=("x", "y", "z"), default="y") parser.add_argument("--max", dest="limit", type=int, default=None, help="Last index to keep along the axis.") parser.add_argument("--max-y", dest="max_y", type=int, default=None, help="Shorthand for --axis y --max N.") parser.add_argument("--keep-above", action="store_true", help="Keep the far side of the cut instead.") parser.add_argument("--name", default="slice") args = parser.parse_args(argv) axis = "y" if args.max_y is not None else args.axis limit = args.max_y if args.max_y is not None else args.limit if limit is None: parser.error("give --max N (with --axis) or --max-y N") slice_schematic(args.input, args.output, axis, limit, args.keep_above, args.name) side = "above" if args.keep_above else "up to" print(f"wrote {args.output} ({side} {axis}={limit})") if __name__ == "__main__": main()

总表

每格是「第一轮 → 修复后」与变化百分比。点表头排序。

effort耗时 stotal tokenoutputreasoning 命令失败命令估算费用
low667 → 776
+16%
1,203,717 → 1,238,036
+3%
18,906 → 21,878
+16%
4,281 → 6,369
+49%
26 → 25
-4%
4 → 5
+25%
$0.411 → $0.437
+7%
medium735 → 802
+9%
1,407,614 → 1,240,852
-12%
20,281 → 22,702
+12%
5,216 → 5,802
+11%
24 → 36
+50%
5 → 5
+0%
$0.460 → $0.449
-2%
high1,568 → 960
-39%
2,061,936 → 1,702,412
-17%
31,020 → 28,198
-9%
7,793 → 7,184
-8%
33 → 28
-15%
1 → 7
+600%
$0.663 → $0.578
-13%
xhigh1,669 → 1,719
+3%
2,443,140 → 2,082,834
-15%
49,051 → 48,241
-2%
17,264 → 18,930
+10%
32 → 30
-6%
4 → 5
+25%
$1.010 → $0.907
-10%

四档合计估算费用 $2.54 → $2.37(-7%)。 费率沿用 GPT-5 档位:新增输入 $1.25/M、缓存输入 $0.125/M、输出 $10/M。


图表

每张图一个量纲,成对柱子是同一档的两轮。

Fig 1. 墙钟耗时(秒)。只有 high 明显下降。
Fig 2. 总 token。medium / high / xhigh 下降,low 略升。
Fig 3. 输出与 reasoning token(千级)。xhigh 的输出几乎没变,说明它的开销不在读环境。
Fig 4. 命令数与失败命令数(次)。medium 的命令数涨了一半,因为它这轮渲染得更细。
Fig 5. 估算费用(美元)。
Fig 6. 探索阶段的新增输入 token —— 这次修复直接瞄准的指标,四档全部下降。

阶段去向

单元格是该阶段的新增输入 token,「第一轮 → 修复后」。

阶段lowmediumhighxhigh
探索环境与 API24,305 → 11,617
-52%
30,431 → 19,014
-38%
38,896 → 21,406
-45%
56,817 → 18,097
-68%
写生成器1,029 → 2,545
+147%
2,638 → 1,482
-44%
105 → 8,789
+8270%
6,630 → 56,947
+759%
渲染看图283 → 12,069
+4165%
508 → 8,769
+1626%
16,251 → 13,368
-18%
17,293 → 3,907
-77%
写测试与跑测试13,140 → 21,082
+60%
14,315 → 17,104
+19%
8,357 → 17,910
+114%
89,952 → 43,907
-51%
修补代码6,158 → 449
-93%
788 → 4,819
+512%
0 → 6,1318,102 → 0
验证导出8,647 → 4,824
-44%
6,506 → 5,012
-23%
8,032 → 1,649
-79%
0 → 5,988
写报告1,962 → 01,023 → 01,565 → 04,600 → 0

读法:探索阶段整体腰斩,写报告阶段归零(这轮取消了),而渲染与测试阶段普遍上升。 low 的渲染从 283 token 涨到 12,069,因为模板让它敢多渲;xhigh 的测试阶段从 92,740 降到 51,539, 因为它不再需要反复回读自己那套校验代码。


产物与质量

effort非空气方块palette state方块种类尺寸渲染张数本轮测试数
low10,031 → 10,59974 → 8955 → 6245x41x35 → 45x40x3512 → 209
medium10,661 → 10,21879 → 8058 → 6045x40x35 → 45x40x3512 → 97
high12,341 → 11,946101 → 10270 → 7047x41x37 → 45x40x359 → 910
xhigh11,992 → 10,654126 → 11478 → 7947x43x37 → 45x41x358 → 1014

规模基本持平,细节略有升降:low 的 palette 从 74 涨到 89,xhigh 从 126 降到 114。 两轮八次跑全部通过打分(产物可加载、库未被改动、symlink 未被替换),自写测试也全部跑绿。

确定性:把八份生成器各跑两遍比 sha256,第一轮 low / high / xhigh 字节稳定、medium 每次都变; 修复后四档全部字节稳定。模板里的时间戳清零是唯一原因。

失败类型变了:第一轮 low 两次 SyntaxError、medium 一次 ModuleNotFoundError, 都来自一次性写大文件;修复后这类只剩两次(medium、high 各一次 SyntaxError,xhigh 一次 ModuleNotFoundError)。 这轮的失败集中在 NBT 断言:high 的 test_nbt_curtains_frames_and_open_book 挂了两次, xhigh 的 test_nbt_curtains_books_inventory_and_frames 挂了四次 —— 正是新 prompt 要求它们用断言验证的那几项。


八个 schematic

左右成对,上一行是第一轮,下一行是修复后。可拖动旋转。

low · 第一轮 · 10,031 方块 · palette 74

low · 修复后 · 10,599 方块 · palette 89

medium · 第一轮 · 10,661 方块 · palette 79

medium · 修复后 · 10,218 方块 · palette 80

high · 第一轮 · 12,341 方块 · palette 101

high · 修复后 · 11,946 方块 · palette 102

xhigh · 第一轮 · 11,992 方块 · palette 126

xhigh · 修复后 · 10,654 方块 · palette 114


渲染图

low 修复后的渲染(20 张)
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
low 第一轮的渲染(12 张)
low_bedroom.png
bedroom.png
low_exterior_front.png
exterior_front.png
low_exterior_rear.png
exterior_rear.png
low_ground.png
ground.png
low_initial_bedroom.png
initial_bedroom.png
low_initial_exterior_front.png
initial_exterior_front.png
low_initial_exterior_rear.png
initial_exterior_rear.png
low_initial_ground.png
initial_ground.png
low_initial_living.png
initial_living.png
low_initial_study.png
initial_study.png
low_living.png
living.png
low_study.png
study.png
medium 修复后的渲染(9 张)
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
medium 第一轮的渲染(12 张)
medium_bedroom.png
bedroom.png
medium_exterior_back.png
exterior_back.png
medium_exterior_front.png
exterior_front.png
medium_first_pass_bedroom.png
first_pass_bedroom.png
medium_first_pass_exterior_back.png
first_pass_exterior_back.png
medium_first_pass_exterior_front.png
first_pass_exterior_front.png
medium_first_pass_ground.png
first_pass_ground.png
medium_first_pass_living.png
first_pass_living.png
medium_first_pass_study.png
first_pass_study.png
medium_ground.png
ground.png
medium_living.png
living.png
medium_study.png
study.png
high 修复后的渲染(9 张)
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
high 第一轮的渲染(9 张)
high_attic.png
attic.png
high_bedroom.png
bedroom.png
high_bedroom_window.png
bedroom_window.png
high_exterior_front.png
exterior_front.png
high_exterior_rear.png
exterior_rear.png
high_ground.png
ground.png
high_initial_bedroom.png
initial_bedroom.png
high_initial_exterior_front.png
initial_exterior_front.png
high_living.png
living.png
xhigh 修复后的渲染(10 张)
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
xhigh 第一轮的渲染(8 张)
xhigh_attic.png
attic.png
xhigh_bedroom.png
bedroom.png
xhigh_bedroom_detail.png
bedroom_detail.png
xhigh_exterior_front_left.png
exterior_front_left.png
xhigh_exterior_rear_right.png
exterior_rear_right.png
xhigh_forge_detail.png
forge_detail.png
xhigh_ground.png
ground.png
xhigh_living.png
living.png

命令轨迹

每条 shell 命令的首行、退出码、耗时,两轮并列。

low 修复后 — 25 条命令,5 条失败
[01] exit=0 0s cat /work/AGENTS.md /opt/tools/template_generator.py [02] exit=0 0s cat /opt/tools/template_render.mjs; ls -la [03] exit=0 0s rg -n 'block_entit|entities|entity' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/schematic.py; rg -n 'class.*|entities|tile_entities' [04] exit=0 0s rg -n 'entities|Entities|TileEntities|regions|Region' /work/generator/MCIO/mcio/litematica.py | head -60; rg -n 'to_litematica' /work/generator/MCIO/m [05] exit=0 0s sed -n '660,690p' /work/generator/MCIO/mcio/litematica.py; sed -n '1,120p' /work/generator/MCIO/mcio/nbt/nbt.py; sed -n '1,75p' /work/generator/MCIO/m [06] exit=0 0s cp /opt/tools/template_generator.py generate.py [07] exit=0 2s chmod u+w generate.py [08] exit=0 0s python - <<'PY' [09] exit=0 0s rg -n 'def material|def block' /work/generator/MCIO/mcio/sketch.py -A45 [10] exit=0 2s python - <<'PY' [11] exit=0 1s python - <<'PY' [12] exit=1 0s mkdir -p test <-- FAILED [13] exit=1 1s python - <<'PY' <-- FAILED [14] exit=0 4s RENDER_SIZE=800 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- previews/bedroom.litematic previews/bedroom [15] exit=0 1s python - <<'PY' [16] exit=1 1s python - <<'PY' <-- FAILED [17] exit=0 4s python - <<'PY' [18] exit=0 6s RENDER_SIZE=900 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- previews/bedroom.litematic previews/bedroom [19] exit=1 1s python - <<'PY' <-- FAILED [20] exit=0 3s cat > preview_rooms.py <<'PY' [21] exit=0 1s python - <<'PY' [22] exit=0 16s cat > render_all.sh <<'SH' [23] exit=1 1s cat >> test/test_structure.py <<'PY' <-- FAILED [24] exit=0 17s rg -n 'y \+ 1, 30|y, 10, x' generate.py [25] exit=0 0s python - <<'PY'
low 第一轮 — 26 条命令,4 条失败
[01] exit=0 0s cat /work/AGENTS.md [02] exit=0 0s ls -la; rg -n 'class LitematicCanvas|def |class Material' /work/generator/MCIO/mcio/sketch.py [03] exit=0 0s cat /work/generator/MCRender/src/headless/index.ts [04] exit=0 0s sed -n '1,175p' /work/generator/MCIO/mcio/sketch.py; rg -n 'def load_schematic|class .*|def ' /work/generator/MCIO/mcio/schematic.py | head -65; rg -n [05] exit=0 0s sed -n '636,690p' /work/generator/MCIO/mcio/litematica.py; sed -n '819,840p' /work/generator/MCIO/mcio/litematica.py; sed -n '44,65p' /work/generator/ [06] exit=0 0s rg -n 'def save|class File|def write' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt/__init__.py | tail -15; sed -n '800,830p' [07] exit=0 0s sed -n '1315,1335p' /work/generator/MCIO/mcio/litematica.py; head -40 /work/generator/MCIO/mcio/nbt/__init__.py [08] exit=1 0s cat > generate_lodge.py <<'PY' <-- FAILED [09] exit=1 0s python - <<'PY' <-- FAILED [10] exit=0 3s sed -i "221s/$/})/" generate_lodge.py [11] exit=0 2s cat > render_previews.mjs <<'JS' [12] exit=0 0s tail -8 previews/render.log; rg --files previews | head -20 [13] exit=1 0s python - <<'PY' <-- FAILED [14] exit=0 1s python - <<'PY' [15] exit=0 2s sed -i "s/'study': (30, 22, 18)/'study': (31, 22, 18)/" generate_lodge.py [16] exit=0 0s rg -n 'lightDirection|ambientIntensity' /work/generator/MCRender/src -g '*glsl*' -g '*.ts' | head -35; rg -n 'shader|diffuse|dot\(' /work/generator/MC [17] exit=1 0s rg -n 'uLight|ambient|diffuse|lightDirection' /work/generator/MCRender/src/render* /work/generator/MCRender/src/Renderer.ts /work/generator/MCRender/s <-- FAILED [18] exit=0 0s rg -n 'dot\(.*[Ll]ight|uAmbient|uLight|aNormal' /work/generator/MCRender/src | head -35 [19] exit=0 3s sed -i "s/ambientIntensities: \[.85\]/ambientIntensities: [.75]/; s/vec3.fromValues(-.5,1,-.7)/vec3.fromValues(.12,.25,.10)/" render_previews.mjs [20] exit=0 3s python - <<'PY' [21] exit=0 0s cat previews/tests.txt; tail -3 previews/render.log [22] exit=0 2s python - <<'PY' [23] exit=0 3s python - <<'PY' [24] exit=0 0s cat previews/tests.txt [25] exit=0 0s sed -i "s/lecterns\[0\]\['Book'\]\['count'\] == 1/int(lecterns[0]['Book']['count']) == 1/" test/test_structure.py [26] exit=0 0s cat > REPORT.md <<'MD'
medium 修复后 — 36 条命令,5 条失败
[01] exit=0 0s cat /work/AGENTS.md /opt/tools/template_generator.py [02] exit=0 0s cat /opt/tools/template_render.mjs [03] exit=1 0s rg -n 'block_entities|entities|tile_entities' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio -g '*litematic*' <-- FAILED [04] exit=1 0s rg -n 'entity|Entity' /work/generator/MCIO/mcio/sketch.py <-- FAILED [05] exit=0 0s rg --files /work/generator/MCIO/mcio | head -35 [06] exit=0 0s rg -n 'entities|tile_entities|block_entities' /work/generator/MCIO/mcio | head -65 [07] exit=0 0s cat /work/generator/MCIO/mcio/litematica.py | head -230 [08] exit=0 0s rg -n 'TileEntities|Entities|def save|to_nbt' /work/generator/MCIO/mcio/litematica.py [09] exit=0 0s cat /work/generator/MCIO/mcio/nbt/nbt.py | head -125 [10] exit=0 0s cp /opt/tools/template_generator.py generate.py [11] exit=0 0s cat > /tmp/build_methods.txt <<'PY' [12] exit=0 2s chmod u+w generate.py render.mjs [13] exit=0 2s python - <<'PY' [14] exit=0 0s python - <<'PY' [15] exit=0 1s python - <<'PY' [16] exit=2 0s mkdir -p test <-- FAILED [17] exit=1 0s python - <<'PY' <-- FAILED [18] exit=0 0s python - <<'PY' [19] exit=0 1s python - <<'PY' [20] exit=0 0s cat /opt/tools/cutaway.py | head -155 [21] exit=0 1s python - <<'PY' [22] exit=0 0s python - <<'PY' [23] exit=0 3s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/bedroom_cutaway.litemati [24] exit=0 3s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/ground_cutaway.litematic [25] exit=0 3s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/living_cutaway.litematic [26] exit=0 3s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/study_cutaway.litematic [27] exit=1 1s python - <<'PY' <-- FAILED [28] exit=0 0s python - <<'PY' [29] exit=0 1s python - <<'PY' [30] exit=0 4s python - <<'PY' [31] exit=0 0s cat > README.md <<'EOF' [32] exit=0 4s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/study_cutaway.litematic [33] 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 [34] 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 [35] 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 [36] exit=0 0s stat -c '%n %s bytes' output.litematic generate.py test/test_structure.py
medium 第一轮 — 24 条命令,5 条失败
[01] exit=0 0s cat /work/AGENTS.md; pwd; ls -la; ls /work/generator/MCIO/mcio; cat /work/generator/MCRender/src/headless/index.ts [02] exit=0 0s rg -n 'class LitematicCanvas|def |entities' /work/generator/MCIO/mcio/sketch.py | tail -65; rg -n 'def load_schematic|class |def ' /work/generator/MCI [03] exit=0 0s sed -n '1,175p' /work/generator/MCIO/mcio/sketch.py; sed -n '44,85p' /work/generator/MCIO/mcio/schematic.py; rg -n 'entit|class|def save' /work/genera [04] exit=0 0s sed -n '636,706p' /work/generator/MCIO/mcio/litematica.py; sed -n '1250,1338p' /work/generator/MCIO/mcio/litematica.py; rg -n 'Entities|TileEntities' [05] exit=1 0s cat > generate.py <<'PY' <-- FAILED [06] exit=0 0s rg -n 'import.*NBTFile' /work/generator/MCIO/mcio/litematica.py; rg --files /work/generator/MCIO/mcio/nbt | head -15 [07] exit=1 0s sed -i 's/from mcio.nbt.file import NBTFile/from mcio.nbt.nbt import File as NBTFile/' generate.py <-- FAILED [08] exit=0 3s sed -i "s/(16,31,26)/(16,32,26)/; s/radius=max(1,(height-y)\/\/5)/radius=min(2,max(1,(height-y)\/\/5))/" generate.py [09] exit=0 0s tail -8 previews/render.log; rg --files previews | head -20 [10] exit=1 0s mkdir -p test previews/first_pass <-- FAILED [11] exit=1 0s python - <<'PY' <-- FAILED [12] exit=0 0s rg -n 'sin|cos|sphericalCameraPose' /work/generator/MCRender/src/renderHelpers.ts | head -20 [13] exit=0 0s sed -n '59,83p' /work/generator/MCRender/src/renderHelpers.ts; rg -n 'ambientIntensity|lightDirection' /work/generator/MCRender/src/StructureRenderer. [14] exit=0 0s rg -n 'ambientIntensity|lightDirection|uLight' /work/generator/MCRender/src -g '*.glsl' -g '*.ts' | head -35 [15] exit=0 3s python - <<'PY' [16] exit=0 0s sed -n '175,235p' test/test_structure.py; sed -n '10,52p' generate.py; tail -35 generate.py [17] exit=0 0s sed -n '140,172p' test/test_structure.py; rg -n 'x1, x2, z1|def door' generate.py [18] exit=0 0s sed -n '183,207p' generate.py [19] exit=1 0s python - <<'PY' <-- FAILED [20] exit=0 0s sed -i "s/b.put(13, 11, 26, 'smoker'/b.put(15, 11, 26, 'smoker'/" generate.py [21] exit=0 0s sed -n '87,116p' generate.py [22] exit=0 3s python - <<'PY' [23] exit=0 0s PYTHONDONTWRITEBYTECODE=1 python - <<'PY' [24] exit=0 0s cat > REPORT.md <<'MD'
high 修复后 — 28 条命令,7 条失败
[01] exit=0 0s cat /work/AGENTS.md /opt/tools/template_generator.py /opt/tools/template_render.mjs [02] exit=2 0s rg -n 'block_entit|entities|class Litemat|class.*Entity' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/schematic /work/generator/MCIO/ <-- FAILED [03] exit=0 0s sed -n '1,200p' /opt/tools/cutaway.py; rg -n 'tile_entities|entities|block_entities' /work/generator/MCIO/mcio -g '*.py' | head -70 [04] exit=0 0s sed -n '1,220p' /work/generator/MCIO/mcio/sketch.py; rg -n 'class|entities|tile' /work/generator/MCIO/mcio/litematica.py /work/generator/MCIO/mcio/nbt [05] exit=0 0s sed -n '990,1085p' /work/generator/MCIO/mcio/litematica.py; sed -n '1,180p' /work/generator/MCIO/mcio/nbt/nbt.py; /opt/venv/bin/python - <<'PY' [06] exit=0 0s cp /opt/tools/template_generator.py generate.py [07] exit=0 0s cat > /tmp/lodge_class.py <<'PY' [08] exit=0 3s chmod u+w generate.py render.mjs [09] exit=0 0s sed -n '210,250p' /work/generator/MCIO/mcio/nbt/nbt.py; sed -n '1314,1345p' /work/generator/MCIO/mcio/litematica.py [10] exit=127 0s sed -n '30,43p' /work/generator/MCIO/mcio/litematica.py; sed -n '678,700p' /work/generator/MCIO/mcio/litematica.py; file output.litematic <-- FAILED [11] exit=1 0s /opt/venv/bin/python - <<'PY' <-- FAILED [12] exit=1 0s /opt/venv/bin/python - <<'PY' <-- FAILED [13] exit=0 0s /opt/venv/bin/python - <<'PY' [14] exit=2 0s /opt/venv/bin/python - <<'PY' <-- FAILED [15] exit=1 1s /opt/venv/bin/python - <<'PY' <-- FAILED [16] exit=0 0s /opt/venv/bin/python - <<'PY' [17] exit=1 1s /opt/venv/bin/python - <<'PY' <-- FAILED [18] exit=0 1s cat > make_previews.py <<'PY' [19] exit=0 1s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/attic_study.litematic -- [20] exit=0 1s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/living_dining.litematic [21] exit=0 1s /opt/venv/bin/python - <<'PY' [22] exit=0 0s /opt/venv/bin/python - <<'PY' [23] exit=0 0s /opt/venv/bin/python - <<'PY' [24] exit=0 2s /opt/venv/bin/python - <<'PY' [25] exit=0 3s /opt/venv/bin/python - <<'PY' [26] exit=0 4s /opt/venv/bin/python - <<'PY' [27] exit=0 5s /opt/venv/bin/python - <<'PY' [28] exit=0 0s /opt/venv/bin/python - <<'PY'
high 第一轮 — 33 条命令,1 条失败
[01] exit=0 0s cat /work/AGENTS.md [02] exit=0 0s ls -la; rg --files /work/generator/MCIO/mcio /work/generator/MCRender/src/headless [03] exit=0 0s sed -n '1,260p' /work/generator/MCIO/mcio/sketch.py [04] exit=0 0s cat /work/generator/MCRender/src/headless/index.ts [05] exit=0 0s sed -n '1,240p' /work/generator/MCIO/mcio/schematic.py; sed -n '1,180p' /work/generator/MCIO/mcio/litematica.py [06] exit=0 0s rg -n 'tile_entities|entities|TileEntities|Entities|def save|class LitematicaSchematic' /work/generator/MCIO/mcio/litematica.py; rg -n 'export functio [07] exit=0 0s sed -n '660,820p' /work/generator/MCIO/mcio/litematica.py; sed -n '940,1020p' /work/generator/MCRender/src/headless/index.ts; cat /work/generator/MCIO [08] exit=0 0s sed -n '1310,1345p' /work/generator/MCIO/mcio/litematica.py; sed -n '1128,1157p' /work/generator/MCRender/src/headless/index.ts; sed -n '1220,1245p' / [09] exit=0 0s rg --files -g AGENTS.md /work/generator /work/.agent 2>/dev/null | head -30; rg -n 'sphericalCameraPose|Math.cos|Math.sin' /work/generator/MCRender/sr [10] exit=0 0s cat > generate_lodge.py <<'PY' [11] exit=0 0s sed -n '59,85p' /work/generator/MCRender/src/renderHelpers.ts; sed -n '1150,1185p' /work/generator/MCRender/src/headless/index.ts [12] exit=0 3s cat > render_previews.mjs <<'JS' [13] exit=1 0s mkdir -p test <-- FAILED [14] exit=0 0s python - <<'PY' [15] exit=0 0s rg -n 'ambientIntensity|lightDirection' /work/generator/MCRender/src -g '*glsl*' -g '*.ts' | head -50 [16] exit=0 1s python - <<'PY' [17] exit=0 2s python - <<'PY' [18] exit=0 0s rg --files /work/generator/MCRender/resources | head -25; tail -20 previews/render.log [19] exit=0 1s python - <<'PY' [20] exit=0 2s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render_previews.mjs > previews/render.log 2>&1 [21] exit=0 1s python - <<'PY' [22] exit=0 0s rg -n 'banner|item_frame|lectern' /work/generator/MCRender/src -g '*.ts' | head -35; sed -n '35,105p' generate_lodge.py [23] exit=0 0s sed -n '692,716p' /work/generator/MCRender/src/render/SpecialRenderer.ts; rg -n 'def cutaways' generate_lodge.py [24] exit=0 3s python - <<'PY' [25] exit=0 1s python -B - <<'PY' [26] exit=0 0s rg -n 'TileEntities|block_entities|tileEntities|patterns' /work/generator/MCRender/src/Schematics.ts /work/generator/MCRender/src/Structure.ts /work/g [27] exit=0 0s python -B - <<'PY' [28] exit=0 0s rg -n 'nbt|getBlock' /work/generator/MCRender/src/MultiStructure.ts /work/generator/MCRender/src/Structure.ts /work/generator/MCRender/src/StructureRe [29] exit=0 0s sed -n '20,45p' /work/generator/MCRender/src/MultiStructure.ts; rg -n 'addBlock|initStructure|nbt' /work/generator/MCRender/src/headless/index.ts | ta [30] exit=0 0s sed -n '115,173p' /work/generator/MCRender/src/render/ChunkBuilder.ts; sed -n '750,790p' /work/generator/MCRender/src/render/SpecialRenderer.ts; sed - [31] exit=0 0s python -B - <<'PY' [32] exit=0 0s cat > REPORT.md <<'MD' [33] exit=0 0s python -B - <<'PY'
xhigh 修复后 — 30 条命令,5 条失败
[01] exit=0 0s pwd && rg --files -g 'AGENTS.md' -g 'ENVIRONMENT.md' -g '*template*' /work /opt/tools | head -60 [02] exit=0 0s cat /work/AGENTS.md && cat /opt/tools/template_generator.py && cat /opt/tools/template_render.mjs [03] exit=0 0s rg -n 'block_entities|entities|to_litematica|class Litematic' /work/generator/MCIO/mcio/sketch.py /work/generator/MCIO/mcio/schematic.py /work/generat [04] exit=0 0s rg --files /work/generator/MCIO/mcio | head -55 && sed -n '1,160p' /opt/tools/cutaway.py && sed -n '1,100p' /opt/tools/render_schematic.mjs [05] exit=0 0s sed -n '1,220p' /work/generator/MCIO/mcio/sketch.py && sed -n '1,260p' /work/generator/MCIO/mcio/litematica.py [06] exit=0 0s /opt/venv/bin/python - <<'PY' [07] exit=0 0s rg -n 'entities|tile_entities|def save|def to_nbt|class LitematicaSchematic' /work/generator/MCIO/mcio/litematica.py && sed -n '850,1090p' /work/gener [08] exit=0 0s sed -n '636,715p' /work/generator/MCIO/mcio/litematica.py && sed -n '1290,1405p' /work/generator/MCIO/mcio/litematica.py && rg -n 'def write|def save' [09] exit=0 0s cp /opt/tools/template_generator.py generate.py && cp /opt/tools/template_render.mjs render.mjs && mkdir -p test previews [10] exit=0 0s cat > /tmp/lodge_methods.txt <<'PY' [11] exit=1 0s chmod u+w generate.py render.mjs <-- FAILED [12] exit=0 0s /opt/venv/bin/python - <<'PY' [13] exit=0 2s RENDER_SIZE=800 xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script render.mjs -- output.litematic previews [14] exit=0 0s /opt/venv/bin/python - <<'PY' [15] exit=1 0s cat > test/test_lodge.py <<'PY' <-- FAILED [16] exit=1 1s /opt/venv/bin/python - <<'PY' <-- FAILED [17] exit=0 2s /opt/venv/bin/python - <<'PY' [18] exit=1 0s cat > make_previews.py <<'PY' <-- FAILED [19] exit=0 0s cat previews/front_other.camera.json [20] exit=0 0s /opt/venv/bin/python - <<'PY' [21] exit=0 1s xvfb-run -a /work/generator/MCRender/node_modules/.bin/vite-node --script /opt/tools/render_schematic.mjs -- --input previews/bedroom_cutaway.litemati [22] exit=0 1s cat > /tmp/lodge_polish.txt <<'PY' [23] exit=0 3s /opt/venv/bin/python - <<'PY' [24] exit=0 1s cat > /tmp/lodge_landscape.txt <<'PY' [25] exit=0 5s /opt/venv/bin/python - <<'PY' [26] exit=0 0s sed -n '440,510p' generate.py && sed -n '760,880p' generate.py && sed -n '220,290p' test/test_lodge.py [27] exit=0 2s /opt/venv/bin/python - <<'PY' [28] exit=1 0s cat > README.md <<'MD' <-- FAILED [29] exit=0 3s /opt/venv/bin/python make_previews.py [30] exit=0 0s /opt/venv/bin/python - <<'PY'
xhigh 第一轮 — 32 条命令,4 条失败
[01] exit=0 0s pwd && rg --files -g 'AGENTS.md' -g '*.py' -g '*.mjs' -g 'README*' . /work/generator/MCIO /work/generator/MCRender/src/headless /work 2>/dev/null | he [02] exit=0 0s cat /work/AGENTS.md [03] exit=0 0s sed -n '1,260p' /work/generator/MCIO/mcio/sketch.py [04] exit=0 0s for p in /work/generator/AGENTS.md /work/generator/workspace/AGENTS.md; do if test -f "$p"; then cat "$p"; fi; done [05] exit=0 0s sed -n '1,260p' /work/generator/MCIO/mcio/schematic.py && sed -n '1,220p' /work/generator/MCRender/src/headless/index.ts [06] exit=0 0s rg -n 'entities|Entities|tile_entities|TileEntities|block_entities|def save|DATA_VERSION' /work/generator/MCIO/mcio/litematica.py [07] exit=0 0s python - <<'PY' [08] exit=0 0s rg -n 'function createRenderScene|function renderLitematicWithMetadata|cameraDistance|objectCenter|ambientIntensity' /work/generator/MCRender/src/head [09] exit=0 0s sed -n '765,850p' /work/generator/MCRender/src/headless/index.ts [10] exit=0 0s sed -n '1128,1258p' /work/generator/MCRender/src/headless/index.ts [11] exit=1 0s cat > generate_lodge.py <<'PY' <-- FAILED [12] exit=0 4s python - <<'PY' [13] exit=1 0s mkdir -p test <-- FAILED [14] exit=0 0s python - <<'PY' [15] exit=0 0s rg -n 'lightDirection|uLight|ambient' /work/generator/MCRender/src/StructureRenderer.ts /work/generator/MCRender/src/render* /work/generator/MCRender/ [16] exit=1 0s python - <<'PY' <-- FAILED [17] exit=0 3s python - <<'PY' [18] exit=0 1s rg -n 'lightDirection|uLight|ambient' /work/generator/MCRender/src/StructureRenderer.ts /work/generator/MCRender/src/Renderer.ts | head -40 [19] exit=0 0s rg -n 'banner|patterns|TileEntities|ItemFrame' /work/generator/MCRender/src/Schematics.ts /work/generator/MCRender/src/StructureRenderer.ts /work/gene [20] exit=0 3s python - <<'PY' [21] exit=0 0s rg -n 'def save|def write' /work/generator/MCIO/mcio/nbt/nbt.py [22] exit=0 0s sed -n '223,242p' /work/generator/MCIO/mcio/nbt/nbt.py [23] exit=1 1s python - <<'PY' <-- FAILED [24] exit=0 0s python - <<'PY' [25] exit=0 5s python - <<'PY' [26] exit=0 0s cat previews/test_results.txt [27] exit=0 0s python - <<'PY' [28] exit=0 1s python - <<'PY' [29] exit=0 5s python -m yapf --style /work/.style.yapf -i generate_lodge.py structural_checks.py test/test_lodge.py [30] exit=0 0s cat previews/generation.log [31] exit=0 0s cat > REPORT.md <<'MD' [32] exit=0 0s PYTHONDONTWRITEBYTECODE=1 python - <<'PY'

相同的部分

不同的部分


token 花在哪

两种切法都做:按用途聚合,和按时间逐请求切。归因方式是看每个请求触发的命令, 取其中最强的动作(跑 pytest > 渲染 > 写或改生成器 > 写测试文件 > 格式化 > 验证 > 读源码)。

Fig 7. 按用途聚合的新增输入 token(缓存命中的输入不计入此图)。每档两行,上为第一轮、下为修复后。 hover 看该段是什么事、几个请求、几条命令、折合多少钱。
Fig 8. 按用途聚合的输出 token。写生成器那一段在 xhigh 明显变粗:模板让它把预算从"读"挪到了"写"。

逐请求细切(按时间)

low 修复后逐请求细切 — 27 个请求
每段是一个模型请求,左起为时间顺序。hover 看该请求触发了什么命令、花了多少 token、折合多少钱。
medium 修复后逐请求细切 — 29 个请求
每段是一个模型请求,左起为时间顺序。hover 看该请求触发了什么命令、花了多少 token、折合多少钱。
high 修复后逐请求细切 — 30 个请求
每段是一个模型请求,左起为时间顺序。hover 看该请求触发了什么命令、花了多少 token、折合多少钱。
xhigh 修复后逐请求细切 — 29 个请求
每段是一个模型请求,左起为时间顺序。hover 看该请求触发了什么命令、花了多少 token、折合多少钱。

第一轮的逐请求细切在 上一份报告

花费估算,两轮对照

费率:新增输入 $1.25/M,缓存输入 $0.125/M,输出 $10/M(输出含 reasoning)。 这是估算gpt-6-astra 没有公开定价,八次跑都走订阅额度,真实支出为 0。

effort缓存输入缓存费新增输入新增输入费 输出输出费合计
low1,119,488 → 1,156,992$0.140 → $0.14565,323 → 59,166$0.082 → $0.07418,906 → 21,878$0.189 → $0.219$0.411 → $0.437
+7%
medium1,313,024 → 1,156,608$0.164 → $0.14574,309 → 61,542$0.093 → $0.07720,281 → 22,702$0.203 → $0.227$0.460 → $0.449
-2%
high1,943,040 → 1,597,312$0.243 → $0.20087,876 → 76,902$0.110 → $0.09631,020 → 28,198$0.310 → $0.282$0.663 → $0.578
-13%
xhigh2,198,400 → 1,883,008$0.275 → $0.235195,689 → 151,585$0.245 → $0.19049,051 → 48,241$0.490 → $0.482$1.010 → $0.907
-10%

结构没变:低档仍然把钱花在重复喂进去的上下文上,高档仍然把钱花在输出上。 变化在于 xhigh 的新增输入费从 $0.245 降到 $0.226,而输出费只从 $0.490 降到 $0.482 —— 省掉的是读环境,写代码那部分一分没省。


问题分类对比

类别lowmediumhighxhigh合计
A 写文件时的语法或导入错2 → 01 → 10 → 10 → 13 → 3
B 坐标越界1 → 01 → 0
C 自写测试失败1 → 53 → 21 → 23 → 38 → 12
D shell 与工具用法错1 → 00 → 20 → 31 → 5
E 其它(权限、探针断言)1 → 00 → 10 → 11 → 2
Fig 9. 失败命令按类别,四档相加,两轮对照。单位是次数, 统计口径是退出码非零的命令,一条命令只归一类。上面那张表是每档单列的同一份数据。
逐条明细

low

  • 第一轮:#8 SyntaxError;#9 SyntaxError;#13 ModuleNotFoundError、AssertionError、FAILED:test_export_roundtrip_and_entitiestest_two_block_headroom_reachability;#17 no matches found
  • 修复后:#12 AssertionError、FAILED:test_decoration_support_and_nbttest_walkable_routes_and_ladder_clearance;#13 AssertionError、FAILED:test_walkable_routes_and_ladder_clearance;#16 AssertionError、FAILED:test_walkable_routes_and_ladder_clearance;#19 AssertionError、FAILED:test_openings_and_nearby_lights;#23 AssertionError、FAILED:test_roof_join_forge_and_pines

medium

  • 第一轮:#5 ModuleNotFoundError;#7 AssertionError;#10 AssertionError、FAILED:test_no_floating_decorationtest_reachability_and_headroomtest_validators_reject_damage;#11 AssertionError、FAILED:test_no_floating_decoration;#19 AssertionError、FAILED:test_windows_not_obstructed
  • 修复后:#3 无错误关键字;#4 无错误关键字;#16 SyntaxError;#17 AssertionError、FAILED:test_decorations_supported_and_openings_cleartest_envelope_separates_rooms_from_sky;#27 AssertionError、FAILED:test_all_rooms_reachable_with_headroom

high

  • 第一轮:#13 AssertionError、FAILED:test_decoration_support_and_window_clearancetest_enclosure_and_roof_jointest_no_disconnected_blocks
  • 修复后:#2 无错误关键字;#10 command not found;#11 无错误关键字;#12 AssertionError;#14 SyntaxError、AssertionError;#15 AssertionError、FAILED:test_nbt_curtains_frames_and_open_booktest_windows_have_glass_and_clear_openings;#17 AssertionError、FAILED:test_nbt_curtains_frames_and_open_book

xhigh

  • 第一轮:#11 out of bounds;#13 AssertionError、FAILED:test_front_door_reaches_every_room_and_stair_route;#16 AssertionError、FAILED:test_enclosure_and_missing_roof_negative_controltest_no_floating_or_clipping_decorations;#23 AssertionError、FAILED:test_window_openings_are_preserved
  • 修复后:#11 AssertionError;#15 AssertionError、FAILED:test_all_rooms_and_floors_reachable_with_headroomtest_nbt_curtains_books_inventory_and_framestest_solid_geometry_has_no_floating_componentstest_windows_preserved_and_external_faces_clear;#16 AssertionError、FAILED:test_nbt_curtains_books_inventory_and_framestest_solid_geometry_has_no_floating_componentstest_windows_preserved_and_external_faces_clear;#18 AssertionError、FAILED:test_nbt_curtains_books_inventory_and_frames;#28 ModuleNotFoundError

读出来的变化


测试对比

effort测试个数测试体行数测试相关总行数破坏式断言行数
low8 → 9133 → 174182 → 2214 → 1
medium9 → 786 → 165194 → 2315 → 2
high12 → 10154 → 207305 → 3054 → 3
xhigh12 → 14127 → 236505 → 39124 → 5

类别覆盖,两轮对照

单元格是「第一轮 → 修复后」的测试个数与测试体行数。

类别lowmediumhighxhigh
导出回读1 个 / 14 行 → 1 个 / 7 行1 个 / 13 行 → 1 个 / 12 行1 个 / 13 行 → 1 个 / 19 行1 个 / 11 行 → 0 个 / 0 行
状态与尺寸合法性1 个 / 9 行 → 1 个 / 10 行0 个 / 0 行 → 1 个 / 10 行1 个 / 11 行 → 2 个 / 16 行
确定性0 个 / 0 行 → 1 个 / 8 行1 个 / 7 行 → 1 个 / 7 行1 个 / 10 行 → 1 个 / 11 行2 个 / 16 行 → 1 个 / 10 行
门床配对1 个 / 17 行 → 1 个 / 23 行1 个 / 3 行 → 1 个 / 21 行2 个 / 14 行 → 1 个 / 27 行3 个 / 40 行 → 2 个 / 33 行
可达性1 个 / 24 行 → 1 个 / 42 行1 个 / 6 行 → 1 个 / 32 行1 个 / 19 行 → 2 个 / 61 行0 个 / 0 行 → 2 个 / 22 行
封闭性1 个 / 25 行 → 2 个 / 43 行1 个 / 2 行 → 1 个 / 44 行2 个 / 18 行 → 1 个 / 31 行1 个 / 10 行 → 2 个 / 54 行
支撑与悬空1 个 / 22 行 → 1 个 / 28 行1 个 / 23 行 → 1 个 / 28 行2 个 / 51 行 → 0 个 / 0 行1 个 / 8 行 → 2 个 / 44 行
窗洞1 个 / 18 行 → 1 个 / 13 行1 个 / 11 行 → 0 个 / 0 行0 个 / 0 行 → 1 个 / 8 行1 个 / 10 行 → 1 个 / 10 行
照明1 个 / 4 行 → 0 个 / 0 行1 个 / 4 行 → 1 个 / 21 行1 个 / 14 行 → 1 个 / 17 行1 个 / 4 行 → 1 个 / 17 行
实体与 NBT1 个 / 11 行 → 1 个 / 23 行1 个 / 17 行 → 1 个 / 30 行
其它1 个 / 17 行 → 0 个 / 0 行1 个 / 4 行 → 0 个 / 0 行
Fig 10. 按类别统计的测试体行数,四档相加(low + medium + high + xhigh),两轮对照。 单位是行,只算测试函数本身,不含辅助函数与独立校验模块。合计从 500 行涨到 782 行。 上面那张表是每档单列的同一份数据,两者口径不同,逐档相加即可对上。 归类方式:一个测试只进一个类别,按函数名里的首要关键词判定,所以像 test_openings_and_nearby_lights 这种一条测两件事的,全部行数都记在"窗洞"名下。 这也是"可达性"与"封闭性"这轮跳得厉害的原因:第二轮爱写长的合并测试,一条就把整块行数灌进一个类别。

读出来的变化


四份代码

effort生成器行数函数个数与第一轮同名的函数生成器与测试之外的 .py 文件
low354 → 57510 → 136无 → preview_rooms.py 29 行
medium397 → 53611 → 217无 → make_previews.py 38 行
high614 → 75426 → 4115无 → make_previews.py 37 行
xhigh915 → 89932 → 4317structural_checks.py 325 行 → make_previews.py 49 行

结构上的变化

同名函数矩阵

单元格是两份生成器之间同名函数的个数(含类方法,不含类名)。颜色深浅按数值,仅供快速比较。

修复后,四档之间

lowmediumhighxhigh
low13
自身
131313
medium1321
自身
1920
high131939
自身
24
xhigh13202443
自身

第一轮,四档之间

lowmediumhighxhigh
low10
自身
789
medium711
自身
89
high8826
自身
16
xhigh991632
自身

跨轮:行是第一轮,列是修复后

lowmediumhighxhigh
low6889
medium6777
high9131515
xhigh9121517

三张矩阵放在一起读: 第一轮四档彼此只重合 7–16 个函数,最低的 low 与 medium 只有 7; 修复后最低值抬到 13,high 与 xhigh 之间到 24 —— 模板把骨架钉死之后,四档的差异只剩自己加的内容。 修复后 low 那一行全是 13,因为它总共只有 13 个函数,全部来自模板,一个自定义方法都没加, 房间布局全写在 build() 里。 跨轮矩阵显示第一轮的 high/xhigh 与修复后的任意一档都能重合 12–15 个, 而第一轮的 low/medium 只能重合 6–9 个:高 effort 在没有模板时就已经自发写出了接近模板的结构。

修复后四档共有的 13 个函数:__init__boxbuildexportgetinsidemainmaterialputshelltexturetileverify。 其中 12 个来自模板,只有 tile(放置方块实体)是四档各自加的同名方法。

完整源码(修复后)

点标签切换档位。第一轮的源码在 上一份报告

low medium high xhigh
generate.py — 575 行
# 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 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, List as NBTList, Double, Float, IntArray 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: furnished timber and plaster lodge, forge, four connected floors and custom pines", } 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 build(self) -> "Build": """Reconstruct Amberstone Lodge with sealed rooms and circulation.""" self.tiles = [] self.entities = [] self.rooms = [(18, 4, 18), (31, 4, 18), (18, 10, 22), (31, 10, 18), (31, 17, 21), (31, 24, 20)] stone = ['minecraft:stone_bricks'] * 5 + ['minecraft:cobblestone'] * 2 + ['minecraft:andesite', 'minecraft:mossy_stone_bricks'] plaster = ['minecraft:yellow_terracotta'] + ['minecraft:smooth_sandstone'] * 6 + ['minecraft:sandstone'] * 2 roof = ['minecraft:deepslate_tiles'] * 6 + ['minecraft:deepslate_bricks'] * 3 + ['minecraft:polished_deepslate'] timber = ['minecraft:spruce_planks'] * 5 + ['minecraft:dark_oak_planks'] # An irregular landscaped island avoids a rectangular grass display base. for x in range(45): for z in range(35): if ((x - 22) / 23)**4 + ((z - 18) / 18)**4 < 1.05: self.put(x, 0, z, self.rng.choice(['minecraft:grass_block'] * 7 + ['minecraft:coarse_dirt', 'minecraft:podzol'])) self.texture((7, 1, 8, 40, 2, 30), stone) self.texture((8, 3, 9, 39, 3, 29), stone) for x in range(8, 40): for z in (9, 29): self.put(x, 3, z, 'minecraft:stone_brick_slab', type='top') # Wide stair approach and a meandering gravel/ochre path. for z, y in [(8, 2), (7, 1), (6, 0)]: for x in range(28, 35): self.put(x, y, z, 'minecraft:stone_brick_stairs', facing='south') for z in range(0, 7): mid = 31 + (z // 3) % 2 for x in range(mid - 3, mid + 4): if self.rng.random() > .13: self.put(x, 0, z, self.rng.choice(['minecraft:gravel', 'minecraft:coarse_dirt', 'minecraft:packed_mud', 'minecraft:andesite'])) # Lower wing, with stone service floor and timber framed living floor. for y in range(4, 16): pal = stone if y < 9 else plaster for x in range(10, 27): for z in (10, 27): self.put(x, y, z, self.rng.choice(pal)) for z in range(10, 28): for x in (10, 26): self.put(x, y, z, self.rng.choice(pal)) for y in (3, 9, 15): self.texture((10, y, 10, 26, y, 27), timber) # Tall wing: solid gable end walls close every space under the steep roof. for x in range(26, 38): top = 23 + 2 * min(x - 24, 39 - x) for y in range(4, top + 1): for z in (12, 28): self.put(x, y, z, self.rng.choice(stone if y < 9 else plaster)) for x in (26, 37): for y in range(4, 28): for z in range(12, 29): self.put(x, y, z, self.rng.choice(stone if y < 9 else plaster)) for y in (3, 9, 16, 23): self.texture((26, y, 12, 37, y, 28), timber) # Horizontal beams and upright framing continue around hidden facades. for y in (9, 15): for z in (10, 27): self.box((10, y, z, 26, y, z), 'minecraft:stripped_spruce_log', axis='x') for x in (10, 26): self.box((x, y, 10, x, y, 27), 'minecraft:stripped_spruce_log', axis='z') for x in (10, 15, 21, 26): for z in (10, 27): self.box((x, 4, z, x, 15, z), 'minecraft:stripped_spruce_log', axis='y') for z in (12, 28): for x in (26, 31, 37): top = 23 + 2 * min(x - 24, 39 - x) self.box((x, 9, z, x, top, z), 'minecraft:stripped_spruce_log', axis='y') for y in (9, 16, 23, 28, 32): for x in range(26, 38): if y <= 23 + 2 * min(x - 24, 39 - x): self.put(x, y, z, 'minecraft:stripped_spruce_log', axis='x') for x in (26, 37): for z in (12, 18, 23, 28): self.box((x, 9, z, x, 27, z), 'minecraft:stripped_spruce_log', axis='y') for y in (9, 16, 23, 27): self.box((x, y, 12, x, y, 28), 'minecraft:stripped_spruce_log', axis='z') # Low roof, broad eaves and a continuous deck. Ends meet the tower wall. for z in range(8, 30): y = 16 + min(z - 8, 29 - z) // 2 self.texture((8, y, z, 26, y, z), roof) for x in range(8, 27): self.put(x, y + 1, z, 'minecraft:deepslate_tile_stairs', facing='south' if z < 19 else 'north') # Seal the low attic behind the overhanging roof. for z in range(10, 28): top = 16 + min(z - 8, 29 - z) // 2 for y in range(16, top + 1): for x in (10, 26): self.put(x, y, z, 'minecraft:spruce_planks') if z in (10, 27): self.box((10, y, z, 26, y, z), 'minecraft:spruce_planks') # Very steep tower roof, two blocks of rise per column. for x in range(24, 40): y = 23 + 2 * min(x - 24, 39 - x) self.texture((x, y - 1, 10, x, y + 1, 30), roof) for z in range(10, 31): self.put(x, y + 1, z, 'minecraft:deepslate_tile_stairs', facing='east' if x < 32 else 'west') for z in (10, 30): self.put(x, y, z, 'minecraft:polished_deepslate') for z in range(10, 31): for x in (31, 32): self.put(x, 39, z, 'minecraft:deepslate_tile_slab', type='bottom') # Small glazed roof dormer and ladder-accessible reading nook. self.box((17, 18, 11, 22, 18, 15), 'minecraft:spruce_planks') self.shell((17, 19, 11, 22, 21, 15), 'minecraft:spruce_planks') self.box((18, 19, 12, 21, 21, 14), 'minecraft:air') self.texture((16, 22, 10, 23, 22, 16), roof) self.box((16, 23, 10, 23, 23, 10), 'minecraft:deepslate_tile_stairs', facing='south') self.box((16, 23, 11, 23, 23, 16), 'minecraft:deepslate_tile_slab', type='bottom') for yy in range(10, 21): self.put(22, yy, 14, 'minecraft:stripped_spruce_log', axis='y') self.put(21, yy, 14, 'minecraft:ladder', facing='west') self.put(18, 19, 14, 'minecraft:barrel', facing='up') self.put(18, 20, 14, 'minecraft:lantern') self.put(19, 19, 14, 'minecraft:bookshelf') # Glazed openings and outward shutters; no roof or furniture crosses these. self.windows = [] def window(x, y, z, side='north', w=2, h=3): for a in range(w): for b in range(h): p = (x + a, y + b, z) if side in ('north', 'south') else (x, y + b, z + a) self.put(*p, 'minecraft:glass') self.windows.append(p) if side in ('north', 'south'): dz = -1 if side == 'north' else 1 for xx in (x - 1, x + w): for yy in range(y, y + h): self.put(xx, yy, z + dz, 'minecraft:spruce_trapdoor', facing=side, half='bottom', open='true') self.box((x - 1, y - 1, z + dz, x + w, y - 1, z + dz), 'minecraft:spruce_slab', type='top') else: dx = -1 if side == 'west' else 1 for zz in (z - 1, z + w): for yy in range(y, y + h): self.put(x + dx, yy, zz, 'minecraft:spruce_trapdoor', facing=side, open='true') self.box((x + dx, y - 1, z - 1, x + dx, y - 1, z + w), 'minecraft:spruce_slab', type='top') window(18, 19, 11, w=4, h=2) for x in (12, 18, 23): window(x, 11, 10) window(x, 11, 27, 'south') for y in (11, 18, 25, 29): window(33, y, 12, w=2, h=2 if y == 29 else 3) for y in (5, 11, 18, 25): window(28, y, 28, 'south', w=2, h=2) for y in (5, 11, 18): window(37, y, 20, 'east', w=2, h=2) window(10, 11, 19, 'west', w=3) window(18, 5, 10, w=2, h=2) # Proper two-half entry doors and joined rooms at both shared levels. self.doors = [] def door(x, y, z): for half, dy in [('lower', 0), ('upper', 1)]: self.put(x, y + dy, z, 'minecraft:spruce_door', half=half, facing='north', hinge='left', open='false', powered='false') self.doors.append((x, y, z)) door(31, 4, 12) door(10, 4, 16) # Side door faces west. for dy, half in [(0, 'lower'), (1, 'upper')]: self.put(10, 4 + dy, 16, 'minecraft:spruce_door', half=half, facing='west', hinge='left', open='false', powered='false') for y in (4, 10): self.box((26, y, 18, 26, y + 2, 21), 'minecraft:air') self.box((30, 6, 11, 32, 6, 11), 'minecraft:spruce_slab', type='top') # Lower-wing stairs rise southwards, with a full-height open stairwell. for i in range(6): y, z = 4 + i, 18 + i self.box((12, y, z, 13, 12, z), 'minecraft:air') self.box((12, 3, z, 13, y - 1, z), 'minecraft:spruce_planks') for x in (12, 13): self.put(x, y, z, 'minecraft:spruce_stairs', facing='south') # A backed ladder gives continuous access to all tower rooms. for y in range(4, 26): self.put(36, y, 26, 'minecraft:ladder', facing='west') self.put(37, y, 26, 'minecraft:stripped_spruce_log', axis='y') # Parquet floor fields, inset within the timber borders. for x0, x1, z0, z1, y in [(11, 25, 11, 26, 3), (14, 25, 11, 26, 9), (27, 36, 13, 27, 3), (27, 36, 13, 27, 9), (27, 36, 13, 27, 16), (27, 36, 13, 27, 23)]: for xx in range(x0, x1 + 1): for zz in range(z0, z1 + 1): if 'ladder' not in (self.get(xx, y, zz) or ''): self.put(xx, y, zz, 'minecraft:stripped_spruce_log', axis='x' if (xx // 2 + zz // 2) % 2 else 'z') # Exposed ceiling beams, well above the circulation headroom. for yy in (8, 14): for zz in (14, 20, 25): self.box((11, yy, zz, 25, yy, zz), 'minecraft:dark_oak_log', axis='x') for yy in (8, 15, 22, 28): for zz in (15, 22): self.box((27, yy, zz, 36, yy, zz), 'minecraft:dark_oak_log', axis='x') for i in range(6): for yy in range(5 + i, 7 + i): self.box((12, yy, 18 + i, 13, yy, 18 + i), 'minecraft:air') for yy in range(10, 21): self.put(21, yy, 14, 'minecraft:ladder', facing='west') # Ground storage and entry. for x in (15, 16, 22, 23): self.put(x, 4, 25, 'minecraft:barrel', facing='north') self.put(x, 5, 25, 'minecraft:barrel', facing='north') self.put(20, 4, 25, 'minecraft:crafting_table') self.put(21, 4, 25, 'minecraft:chest', facing='north', type='single') self.put(17, 4, 13, 'minecraft:smithing_table') self.put(18, 4, 13, 'minecraft:blast_furnace', facing='south', lit='true') self.put(19, 4, 13, 'minecraft:barrel', facing='up') self.put(19, 5, 13, 'minecraft:lantern') self.box((28, 4, 15, 28, 4, 17), 'minecraft:spruce_stairs', facing='east') self.box((30, 4, 15, 32, 4, 21), 'minecraft:red_carpet') self.put(34, 4, 14, 'minecraft:barrel', facing='up') self.put(34, 5, 14, 'minecraft:potted_fern') self.put(35, 4, 23, 'minecraft:chest', facing='west', type='single') # Living floor: patterned carpet, dining trestle, sofas, kitchen and book wall. for x in range(16, 23): for z in range(15, 22): self.put(x, 10, z, 'minecraft:green_carpet' if (x + z) % 3 else 'minecraft:moss_carpet') self.box((18, 10, 17, 20, 10, 19), 'minecraft:dark_oak_planks') for x in (18, 20): self.put(x, 11, 18, 'minecraft:candle', candles='2', lit='true') for x in (17, 21): for z in (17, 19): self.put(x, 10, z, 'minecraft:spruce_stairs', facing='east' if x == 17 else 'west') for x in range(16, 24): self.put(x, 10, 25, 'minecraft:barrel', facing='up') for x in (17, 21): self.put(x, 11, 25, 'minecraft:potted_blue_orchid') self.put(19, 11, 25, 'minecraft:lantern') self.box((23, 10, 12, 24, 10, 14), 'minecraft:dark_oak_stairs', facing='west') self.box((27, 10, 15, 27, 13, 17), 'minecraft:bookshelf') self.box((28, 10, 24, 32, 10, 24), 'minecraft:dark_oak_stairs', facing='north') self.put(29, 10, 21, 'minecraft:barrel', facing='up') self.put(29, 11, 21, 'minecraft:flower_pot') # Bedroom: raised green double bed, bedside storage and striped curtains. self.box((28, 17, 16, 32, 17, 19), 'minecraft:spruce_planks') for x in (29, 30): self.put(x, 18, 17, 'minecraft:green_bed', part='head', facing='north', occupied='false') self.put(x, 18, 18, 'minecraft:green_bed', part='foot', facing='north', occupied='false') for x in (28, 31): self.put(x, 18, 17, 'minecraft:barrel', facing='up') self.put(x, 19, 17, 'minecraft:lantern' if x == 28 else 'minecraft:potted_blue_orchid') for x in range(28, 33): self.put(x, 17, 20, 'minecraft:spruce_stairs', facing='north') self.box((28, 17, 16, 32, 20, 16), 'minecraft:spruce_planks') self.box((28, 21, 16, 32, 21, 16), 'minecraft:barrel', facing='south') self.box((29, 20, 16, 31, 20, 16), 'minecraft:bookshelf') for x in (32, 35): self.put(x, 20, 13, 'minecraft:white_wall_banner', facing='south') self.tiles.append(self.tile('banner', x, 20, 13, patterns=NBTList[Compound]([Compound({'pattern': String('minecraft:stripe_left'), 'color': String('green')}), Compound({'pattern': String('minecraft:stripe_right'), 'color': String('green')}), Compound({'pattern': String('minecraft:stripe_center'), 'color': String('green')})]))) self.box((33, 17, 13, 35, 17, 13), 'minecraft:dark_oak_stairs', facing='south') self.put(34, 17, 24, 'minecraft:chest', facing='north', type='single') self.box((29, 17, 22, 33, 17, 24), 'minecraft:green_carpet') # Attic study: sloped ceiling, desk, open book and shelves. self.box((28, 24, 15, 28, 26, 19), 'minecraft:bookshelf') self.box((29, 24, 15, 33, 24, 15), 'minecraft:barrel', facing='south') self.put(31, 25, 15, 'minecraft:lectern', facing='south', has_book='true') book = Compound({'id': String('minecraft:written_book'), 'count': Int(1), 'components': Compound({'minecraft:written_book_content': Compound({'title': String('Amberstone Ledger'), 'author': String('The Lodgekeeper'), 'generation': Int(0), 'resolved': Byte(1), 'pages': NBTList[Compound]([Compound({'raw': String('{"text":"Amberstone Lodge\\nProvisions, ore and woodland notes."}')})])})})}) self.tiles.append(self.tile('lectern', 31, 25, 15, Book=book, Page=Int(0))) self.put(33, 25, 15, 'minecraft:candle', candles='3', lit='true') self.put(29, 25, 15, 'minecraft:potted_fern') self.put(31, 24, 17, 'minecraft:spruce_stairs', facing='north') self.box((30, 24, 20, 33, 24, 23), 'minecraft:red_carpet') self.put(34, 24, 24, 'minecraft:chest', facing='north', type='single') self.put(28, 24, 24, 'minecraft:barrel', facing='up') # Central stone chimney breast, independent of the windows and ladder. for y in range(4, 31): self.put(35, y, 18, self.rng.choice(stone)) for y in (4, 10, 17, 24): self.put(35, y, 19, 'minecraft:smoker', facing='south', lit='true') # Greenery climbs the chimney breast in the living hall. self.put(34, 10, 20, 'minecraft:barrel', facing='up') self.put(34, 11, 20, 'minecraft:potted_azalea_bush') for yy in range(11, 15): self.put(35, yy, 17, 'minecraft:vine', south='true') # Lamps mounted on solid pedestals near each floor. for x, y, z in [(23, 4, 16), (15, 4, 23), (33, 4, 23), (33, 10, 16), (15, 10, 14), (24, 10, 23), (33, 17, 21), (30, 24, 25)]: self.put(x, y, z, 'minecraft:barrel', facing='up') self.put(x, y + 1, z, 'minecraft:lantern') # Small belongings make each room group usable, with clear walkways. self.put(15, 6, 25, 'minecraft:potted_fern') self.put(16, 6, 25, 'minecraft:candle', candles='2', lit='true') self.box((19, 4, 18, 21, 4, 21), 'minecraft:brown_carpet') self.put(16, 10, 24, 'minecraft:chest', facing='north', type='single') self.put(34, 10, 24, 'minecraft:chest', facing='north', type='single') self.put(33, 10, 24, 'minecraft:barrel', facing='up') self.put(33, 11, 24, 'minecraft:candle', candles='2', lit='true') self.box((30, 10, 19, 32, 10, 20), 'minecraft:red_carpet') self.put(34, 17, 23, 'minecraft:barrel', facing='up') self.put(34, 18, 23, 'minecraft:candle', candles='2', lit='true') # Item frames on solid interior walls, with deterministic entity UUIDs. for i, (x, y, z) in enumerate([(22, 6, 26), (22, 12, 26), (30, 6, 27), (30, 12, 27), (30, 20, 27), (30, 26, 27)]): self.entities.append(Compound({'id': String('minecraft:item_frame'), 'Pos': NBTList[Double]([Double(x + .5), Double(y + .5), Double(z + .96875)]), 'TileX': Int(x), 'TileY': Int(y), 'TileZ': Int(z), 'Facing': Byte(2), 'Rotation': NBTList[Float]([Float(180), Float(0)]), 'Motion': NBTList[Double]([Double(0)] * 3), 'UUID': IntArray([2026, 923, 100, i]), 'Item': Compound({'id': String(['minecraft:iron_axe', 'minecraft:bread', 'minecraft:compass', 'minecraft:clock', 'minecraft:feather', 'minecraft:map'][i]), 'count': Int(1)}), 'ItemRotation': Byte(0)})) # Covered blacksmith courtyard at the left, with working forge and chimney. self.texture((2, 1, 12, 9, 2, 23), stone) self.texture((2, 3, 12, 9, 3, 23), stone) for x, z in [(2, 12), (2, 23), (9, 12), (9, 23)]: self.box((x, 4, z, x, 8, z), 'minecraft:stripped_spruce_log', axis='y') for x in range(1, 10): y = 8 + (x // 4) self.texture((x, y, 11, x, y, 24), roof) self.box((3, 4, 20, 6, 4, 22), 'minecraft:stone_bricks') self.put(4, 5, 21, 'minecraft:campfire', lit='true', facing='north') self.box((3, 5, 22, 6, 8, 22), 'minecraft:stone_bricks') for x in (3, 6): self.box((x, 5, 20, x, 7, 21), 'minecraft:stone_bricks') self.box((3, 8, 20, 6, 8, 22), 'minecraft:stone_bricks') self.texture((4, 9, 21, 5, 20, 22), stone) self.box((3, 21, 20, 6, 21, 23), 'minecraft:stone_brick_slab', type='bottom') self.put(4, 22, 21, 'minecraft:cobblestone_wall') self.put(5, 22, 22, 'minecraft:cobblestone_wall') self.put(5, 4, 16, 'minecraft:anvil', facing='east') self.put(8, 4, 18, 'minecraft:blast_furnace', facing='west', lit='true') self.put(8, 4, 19, 'minecraft:smithing_table') self.put(3, 4, 13, 'minecraft:barrel', facing='up') self.put(4, 4, 13, 'minecraft:chest', facing='south', type='single') self.put(3, 5, 13, 'minecraft:lantern') # Terrace edge and lamps. for x, z in [(9, 9), (38, 9), (39, 28), (8, 28)]: self.put(x, 4, z, 'minecraft:stone_brick_wall') self.put(x, 5, z, 'minecraft:lantern') # Wall sconces attach with a full timber bracket. for x, y, z in [(29, 6, 11), (36, 6, 11), (11, 13, 9), (25, 13, 9), (38, 20, 17)]: self.put(x, y, z, 'minecraft:spruce_planks') self.put(x, y + 1, z, 'minecraft:lantern') # Climbing leaves and vines adhere to the visible stone and timber pier. for y in range(4, 18): x = 27 if y < 12 else 28 if self.get(x, y, 11) is None: self.put(x, y, 11, 'minecraft:vine', south='true') for x, y, z in [(27, 4, 11), (28, 4, 11), (27, 7, 11), (27, 10, 11), (28, 13, 11), (27, 15, 11)]: if self.get(x, y, 12) and 'glass' not in self.get(x, y, 12): self.put(x, y, z, 'minecraft:oak_leaves', persistent='true') # Handmade pine silhouettes, bare trunks and stepped whorls. for tx, tz, height in [(5, 29, 24), (3, 6, 16), (42, 26, 18)]: self.box((tx, 1, tz, tx, height, tz), 'minecraft:spruce_log', axis='y') for y in range(5, height, 3): radius = max(1, round(4 * (1 - y / height))) for dx, dz in [(1, 0), (-1, 0), (0, 1), (0, -1)]: for step in range(1, max(2, radius)): xx, zz = tx + dx * step, tz + dz * step if self.inside(xx, y, zz): self.put(xx, y, zz, 'minecraft:spruce_log', axis='x' if dx else 'z') for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): if abs(dx) + abs(dz) <= radius + 1 and (dx or dz): xx, zz = tx + dx, tz + dz if self.inside(xx, y + 1, zz): self.put(xx, y + 1, zz, 'minecraft:spruce_leaves', persistent='true') if abs(dx) + abs(dz) >= radius and not self.get(xx, y, zz): self.put(xx, y, zz, 'minecraft:spruce_leaves', persistent='true') for dx, dz in [(1, 0), (-1, 0), (0, 1), (0, -1)]: self.put(tx + dx, y + 2, tz + dz, 'minecraft:spruce_leaves', persistent='true') self.put(tx, height + 1, tz, 'minecraft:spruce_leaves', persistent='true') for x in range(45): for z in range(35): if self.get(x, 0, z) == 'minecraft:grass_block' and not self.get(x, 1, z) and self.rng.random() < .14: self.put(x, 1, z, self.rng.choice(['minecraft:fern'] * 4 + ['minecraft:short_grass', 'minecraft:poppy', 'minecraft:dandelion'])) return self def tile(self, kind, x, y, z, **extra): """Create a positioned vanilla block entity.""" return Compound({'id': String('minecraft:' + kind), 'x': Int(x), 'y': Int(y), 'z': Int(z), **extra}) 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.tiles) region["Entities"] = NBTList[Compound](self.entities) buffer = io.BytesIO() NBTFile(nbt).write(buffer) path.write_bytes(gzip.compress(buffer.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()
preview_rooms.py — 29 行
# Crop the exported schematic for repeatable room inspections. from pathlib import Path from generate import LitematicCanvas, load_schematic, parse_blockstate ROOM_CROPS = { 'bedroom_detail': (26, 16, 12, 37, 21, 28), 'study_detail': (26, 23, 12, 37, 27, 28), 'workshop_detail': (2, 3, 12, 10, 7, 23), 'ground_detail': (10, 3, 10, 37, 7, 28), } def main(): """Export geometry crops for visual checking, never as final deliverables.""" loaded = load_schematic(Path('output.litematic')) ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx) for name, (x0, y0, z0, x1, y1, z1) in ROOM_CROPS.items(): canvas = LitematicCanvas((y1 - y0 + 1, z1 - z0 + 1, x1 - x0 + 1)) for y in range(y0, y1 + 1): for z in range(z0, z1 + 1): for x in range(x0, x1 + 1): state = loaded.palette[int(ids[y, z, x])] canvas.block((y - y0, z - z0, x - x0), parse_blockstate(state)) canvas.save(Path('previews') / f'{name}.litematic', name=name) 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 — 221 行
# Validate the exported lodge, including circulation, support and saved NBT. import sys from collections import deque from pathlib import Path 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 load @pytest.fixture(scope='module') def data(): """Read the actual deliverable and its planned layout.""" build = Build().build() schematic = load_schematic(ROOT / 'output.litematic') ids = schematic.read_flat(0, schematic.volume).reshape(schematic.size_yzx) states = {(x, y, z): schematic.palette[int(ids[y, z, x])] for y in range(40) for z in range(35) for x in range(45)} return build, states, load(ROOT / 'output.litematic') def name(state): return state.split('[')[0].removeprefix('minecraft:') def props(state): return dict(p.split('=') for p in state.split('[')[1].rstrip(']').split(',')) if '[' in state else {} def test_export_and_version(data): b, cells, nbt = data assert int(nbt['MinecraftDataVersion']) == 3955 assert len(nbt['Regions']) == 1 assert b.verify(ROOT / 'output.litematic')['placed_blocks'] > 9500 assert tuple(map(int, nbt['Metadata']['EnclosingSize'].values())) == (45, 40, 35) assert len({name(s) for s in cells.values()}) > 55 def test_doors_and_beds(data): _, cells, _ = data doors = beds = 0 delta = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)} for (x, y, z), state in cells.items(): n, p = name(state), props(state) if n.endswith('_door'): other = cells[x, y + (1 if p['half'] == 'lower' else -1), z] q = props(other) assert name(other) == n and p['half'] != q['half'] assert all(p[k] == q[k] for k in ('facing', 'hinge', 'open', 'powered')) if p['half'] == 'lower': assert name(cells[x, y - 1, z]) != 'air' doors += 1 if n.endswith('_bed'): dx, dz = delta[p['facing']] sign = 1 if p['part'] == 'foot' else -1 other = cells[x + sign * dx, y, z + sign * dz] q = props(other) assert name(other) == n and q['part'] != p['part'] and q['facing'] == p['facing'] assert name(cells[x, y - 1, z]) != 'air' beds += 1 assert doors == 2 and beds == 4 def test_sealed_envelope(data): b, cells, _ = data # Flood only actual air from all six volume faces. Closed doors/glass block entry. outside = set() todo = deque() for p, s in cells.items(): x, y, z = p if name(s) == 'air' and (x in (0, 44) or y in (0, 39) or z in (0, 34)): outside.add(p) todo.append(p) 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 not in outside and name(cells.get(p, 'void')) == 'air': outside.add(p) todo.append(p) # Check every air voxel in occupied volumes, rather than only room centres. for (x, y, z), s in cells.items(): interior = (11 <= x <= 25 and 11 <= z <= 26 and 4 <= y <= 14) or (27 <= x <= 36 and 13 <= z <= 27 and 4 <= y < 23 + 2 * min(x - 24, 39 - x)) if interior and name(s) == 'air': assert (x, y, z) not in outside, f'Leak reaches {(x,y,z)}' def test_walkable_routes_and_ladder_clearance(data): b, cells, _ = data def passable(p): n = name(cells.get(p, 'void')) return n in ('air', 'ladder') or n.endswith(('_carpet', '_door', '_wall_banner')) def support(p): return p in cells and not passable(p) or name(cells.get(p, 'void')) == 'ladder' def stand(p): x, y, z = p return passable(p) and passable((x, y + 1, z)) and (support((x, y - 1, z)) or name(cells[p]) == 'ladder') start = (31, 4, 12) assert stand(start) seen = {start} q = deque([start]) while q: x, y, z = q.popleft() for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)): for dy in (-1, 0, 1): p = x + dx, y + dy, z + dz if p not in seen and stand(p): # On a one-block rise, the current column must also clear the head. if dy == 1 and not passable((x, y + 2, z)): continue seen.add(p) q.append(p) if name(cells[x, y, z]) == 'ladder': for dy in (-1, 1): p = x, y + dy, z if p not in seen and stand(p): seen.add(p) q.append(p) for p in b.rooms + [(36, 24, 26), (29, 18, 20), (31, 24, 18), (5, 4, 15), (20, 19, 14)]: assert p in seen, f'Unreachable: {p}' for y in range(4, 26): assert name(cells[36, y, 26]) == 'ladder' assert name(cells[37, y, 26]) == 'stripped_spruce_log' if y not in (9, 16, 23): assert passable((35, y, 26)) def test_openings_and_nearby_lights(data): b, cells, _ = data for p in b.windows: assert name(cells[p]) == 'glass', f'Obstructed window: {p}' x, y, z = p dx, dz = (1, 0) if x in (10, 37) else (0, 1) for sign in (-1, 1): neighbor = name(cells[x + sign * dx, y, z + sign * dz]) assert neighbor in ('air', 'white_wall_banner'), (p, neighbor) lights = [p for p, s in cells.items() if name(s) == 'lantern' or props(s).get('lit') == 'true'] for room in b.rooms: x, y, z = room assert any(abs(x - a) + abs(z - c) <= 8 and 0 <= h - y <= 3 for a, h, c in lights), room def test_decoration_support_and_nbt(data): _, cells, nbt = data def solid(p): n = name(cells.get(p, 'air')) return n not in ('air', 'vine', 'ladder', 'lantern', 'fern', 'short_grass', 'poppy', 'dandelion') and not n.endswith(('_carpet', '_wall_banner', '_trapdoor', '_stairs', '_slab')) for (x, y, z), s in cells.items(): n, p = name(s), props(s) if n in ('lantern', 'candle', 'flower_pot', 'lectern', 'fern', 'short_grass', 'poppy', 'dandelion') or n.startswith('potted_') or n.endswith('_carpet'): # Fence and wall tops also support lanterns. below = name(cells.get((x, y - 1, z), 'air')) assert solid((x, y - 1, z)) or below.endswith(('_wall', '_fence')), (x, y, z, n, below) if n == 'white_wall_banner': assert solid((x, y, z - 1)) if n == 'vine': assert any(p.get(k) == 'true' and solid((x + dx, y, z + dz)) for k, dx, dz in [('north', 0, -1), ('south', 0, 1), ('east', 1, 0), ('west', -1, 0)]), (x, y, z) region = next(iter(nbt['Regions'].values())) banners = [t for t in region['TileEntities'] if str(t['id']) == 'minecraft:banner'] assert len(banners) == 2 assert all(len(t['patterns']) == 3 and all(str(p['color']) == 'green' for p in t['patterns']) for t in banners) lecterns = [t for t in region['TileEntities'] if str(t['id']) == 'minecraft:lectern'] assert len(lecterns) == 1 and lecterns[0]['Book']['components']['minecraft:written_book_content']['pages'] assert len(region['Entities']) == 6 for e in region['Entities']: x, y, z = (int(e[k]) for k in ('TileX', 'TileY', 'TileZ')) assert solid((x, y, z + 1)) assert int(e['Item']['count']) == 1 def test_reproducible(data): build, _, _ = data from tempfile import TemporaryDirectory with TemporaryDirectory(dir=ROOT / 'test') as temp: tmp_path = Path(temp) a = build.export(tmp_path / 'a.litematic').read_bytes() b = Build().build().export(tmp_path / 'b.litematic').read_bytes() assert a == b == (ROOT / 'output.litematic').read_bytes() def test_valid_vanilla_states(data): import json authority = json.loads(Path('/work/generator/MCRender/resources/1.21.1-blocks.json').read_text()) _, cells, _ = data for state in set(cells.values()): n, p = name(state), props(state) assert n in authority, n allowed = authority[n][0] for key, value in p.items(): assert key in allowed and value in allowed[key], state def test_roof_join_forge_and_pines(data): _, cells, _ = data roof = {p for p, s in cells.items() if 'deepslate' in name(s)} start = (8, 16, 8) seen = {start} todo = deque([start]) 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 roof and p not in seen: seen.add(p) todo.append(p) assert (31, 38, 20) in seen, 'Lower roof must physically join tower roof' assert name(cells[4, 5, 21]) == 'campfire' assert props(cells[4, 5, 21])['lit'] == 'true' assert name(cells[5, 4, 16]) == 'anvil' for x, z, height in ((5, 29, 24), (3, 6, 16), (42, 26, 18)): assert all(name(cells[x, y, z]) == 'spruce_log' for y in range(1, height + 1)) assert name(cells[x, height + 1, z]) == 'spruce_leaves' assert sum(1 for (a, h, c), s in cells.items() if abs(a - x) <= 4 and abs(c - z) <= 4 and h > 4 and name(s) == 'spruce_leaves') > 60

解题方式对比

两轮都一样的

这一轮变了的


还能改什么

  1. NBT 断言也给模板。这轮失败最多的是旗帜图案、书、展示框、容器物品的数据结构断言, 四档各写各的。把这几样的读写与断言写进模板,能吃掉本轮剩下的大半失败。
  2. 渲染变便宜之后会被过度使用。low 渲了 20 张图。给模板一个"够用即止"的默认视角集, 或者在环境文档里给出建议张数。
  3. 探索阶段仍有 12K–21K 新增输入。剩下的主要是读 1.21.1-blocks.json 验证方块状态。 可以提供一个 validate_state(name, **props) 帮助函数,省掉整份注册表的读取。
  4. 时间没降下来。token 降了但耗时没降,说明瓶颈在模型的思考轮次而不是上下文长度。 要压时间得减少迭代轮数,比如让结构校验一次跑完给出全部问题,而不是修一条跑一次。

第一轮的完整报告在 /html/codex-effort-lodge/


数据来自八次跑的 out/report.jsonout/session/raw_session.jsonl, 由容器内 harness 从 codex rollout 提取。