# Generate and verify the furnished Amberstone Lodge schematic for Minecraft Java 1.21.1.
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, Short, Byte, Float, Double, String, IntArray, List as NBTList
from mcio.schematic import load_schematic # noqa: E402
from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402
CONFIG = {
"seed": 20260923,
"size_xyz": (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": "Four-storey timber lodge, vaulted dining wing, working smithy, raised terrace and handmade pines. Java 1.21.1.",
}
STONE = ['minecraft:stone_bricks'] * 6 + ['minecraft:cobblestone'] * 2 + ['minecraft:mossy_stone_bricks']
PLASTER = ['minecraft:smooth_sandstone'] * 8 + ['minecraft:cut_sandstone'] * 2 + ['minecraft:sandstone']
TIMBER = ['minecraft:spruce_planks'] * 10 + ['minecraft:dark_oak_planks', 'minecraft:stripped_spruce_wood']
ROOF = ['minecraft:deepslate_tiles'] * 7 + ['minecraft:deepslate_bricks'] * 2 + ['minecraft:polished_deepslate']
DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}
Position = Tuple[int, int, int]
class Build:
"""Hold a block volume and write it out as a Litematica schematic."""
def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None:
"""Create an empty volume with one seeded random source.
Args:
size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z.
seed (int): Seed for every random decision in this build.
"""
self.size_x, self.size_y, self.size_z = size_xyz
self.rng = random.Random(seed)
self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x))
self.placed: Dict[Position, str] = {}
self.tiles = []
self.entities = []
self.windows = []
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 block_entity(self, pos: Position, kind: str, **data) -> None:
"""Attach block entity data to a local XYZ position.
Args:
pos (Position): Tile position.
kind (str): Vanilla block entity identifier.
**data: Typed NBT fields.
"""
x, y, z = pos
self.tiles.append(Compound(dict(id=String('minecraft:' + kind), x=Int(x), y=Int(y), z=Int(z), **data)))
def frame(self, pos: Position, facing: str, item: str) -> None:
"""Hang an item frame on the solid block behind its facing.
Args:
pos (Position): Air cell occupied by the frame.
facing (str): Cardinal outward direction.
item (str): Displayed vanilla item.
"""
x, y, z = pos
dx, dz = DIRECTIONS[facing]
yaw = {'north': 180., 'south': 0., 'west': 90., 'east': 270.}[facing]
self.entities.append(Compound({
'id': String('minecraft:item_frame'),
'Pos': NBTList[Double]([x + .5 - dx * .46875, y + .5, z + .5 - dz * .46875]),
'Motion': NBTList[Double]([0., 0., 0.]),
'Rotation': NBTList[Float]([yaw, 0.]),
'TileX': Int(x),
'TileY': Int(y),
'TileZ': Int(z),
'Facing': Byte({
'north': 2,
'south': 3,
'west': 4,
'east': 5
}[facing]),
'Item': Compound({
'id': String('minecraft:' + item),
'count': Int(1)
}),
'ItemRotation': Byte(0),
'ItemDropChance': Float(1.),
'UUID': IntArray([self.rng.randint(-2147483648, 2147483647) for _ in range(4)]),
}))
def storage(self, x: int, y: int, z: int, kind: str = 'barrel', facing: str = 'north', item: str = 'bread') -> None:
"""Place usable storage with a small themed inventory.
Args:
x (int): X position.
y (int): Y position.
z (int): Z position.
kind (str): Barrel or chest.
facing (str): Front direction.
item (str): Stored item.
"""
props = dict(facing=facing)
if kind == 'barrel':
props['open'] = 'false'
else:
props.update(type='single', waterlogged='false')
self.put(x, y, z, 'minecraft:' + kind, **props)
self.block_entity((x, y, z), kind, Items=NBTList[Compound]([Compound({'Slot': Byte(0), 'id': String('minecraft:' + item), 'count': Int(16)})]))
def door(self, x: int, y: int, z: int, facing: str, hinge: str = 'left') -> None:
"""Install a matched, closed, two-block spruce door.
Args:
x (int): X position.
y (int): Lower-half height.
z (int): Z position.
facing (str): Door facing.
hinge (str): Hinge side.
"""
for half, dy in [('lower', 0), ('upper', 1)]:
self.put(x, y + dy, z, 'minecraft:spruce_door', facing=facing, hinge=hinge, half=half, open='false', powered='false')
def lamp(self, x: int, y: int, z: int, hanging: bool = False) -> None:
"""Place a lantern at a planned supported position.
Args:
x (int): X position.
y (int): Y position.
z (int): Z position.
hanging (bool): Attach to the block above.
"""
self.put(x, y, z, 'minecraft:lantern', hanging=str(hanging).lower(), waterlogged='false')
def window(self, axis: str, fixed: int, a: int, b: int, bottom: int, top: int, outward: str) -> None:
"""Glaze an opening with projecting timber casing and paired shutters.
Args:
axis (str): Vary X on a Z wall, or vary Z on an X wall.
fixed (int): Fixed wall coordinate.
a (int): First varying coordinate.
b (int): Last varying coordinate.
bottom (int): Bottom glass height.
top (int): Top glass height.
outward (str): Exterior-facing direction.
"""
dx, dz = DIRECTIONS[outward]
for u in range(a - 1, b + 2):
for y in range(bottom - 1, top + 2):
x, z = (u, fixed) if axis == 'x' else (fixed, u)
border = u in (a - 1, b + 1) or y in (bottom - 1, top + 1)
if border:
self.put(x, y, z, 'minecraft:stripped_spruce_log', axis='x' if y in (bottom - 1, top + 1) else 'y')
if y == bottom - 1:
self.put(x + dx, y, z + dz, 'minecraft:spruce_slab', type='top', waterlogged='false')
else:
self.put(x, y, z, 'minecraft:glass')
self.windows.append((x, y, z, outward))
for u in (a - 1, b + 1):
for y in range(bottom, top + 1):
x, z = (u, fixed) if axis == 'x' else (fixed, u)
self.put(x + dx, y, z + dz, 'minecraft:spruce_trapdoor', facing=outward, half='bottom', open='true', powered='false', waterlogged='false')
def landscape(self) -> None:
"""Lay the terrain, irregular approach, and stepped masonry terrace."""
self.box((0, 0, 0, 44, 0, 34), 'minecraft:stone')
self.texture((0, 1, 0, 44, 1, 34), ['minecraft:grass_block'] * 14 + ['minecraft:moss_block', 'minecraft:coarse_dirt'])
for z in range(0, 7):
center = 17 - z // 3
for x in range(center - 2, center + 3):
if abs(x - center) < 2 or self.rng.random() < .72:
self.put(x, 1, z, self.rng.choice(STONE + ['minecraft:gravel', 'minecraft:andesite']))
# A broad landing and a narrower return make the terrace edge less rectangular.
self.texture((6, 2, 7, 38, 4, 13), STONE)
self.texture((8, 2, 6, 36, 4, 6), STONE)
for x in range(7, 39):
self.put(x, 4, 7, 'minecraft:stone_bricks')
for z, y in [(3, 2), (4, 3), (5, 4)]:
self.texture((13, 1, z, 18, y - 1, z), STONE)
self.box((13, y, z, 18, y, z), 'minecraft:stone_brick_stairs', facing='south', half='bottom', shape='straight', waterlogged='false')
for x in (10, 21, 27, 37):
self.box((x, 5, 6, x, 5, 6), 'minecraft:stone_brick_wall', up='true', north='none', south='none', east='none', west='none', waterlogged='false')
self.lamp(x, 6, 6)
for a, b in [(8, 11), (20, 25), (28, 35)]:
for x in range(a, b + 1):
self.put(x, 5, 6, 'minecraft:spruce_fence', east='true', west='true', north='false', south='false', waterlogged='false')
# Lantern piers replace the fence at their coordinates.
for x in (10, 21, 27):
self.put(x, 5, 6, 'minecraft:stone_bricks')
def architecture(self) -> None:
"""Assemble the connected stone base, timber floors, plaster walls, and roofs."""
for x0, z0, x1, z1, top in [(7, 10, 23, 28, 22), (23, 12, 37, 28, 16)]:
self.texture((x0, 2, z0, x1, 4, z1), STONE)
for y in range(5, top):
pal = STONE if y <= 9 else PLASTER
for x in range(x0, x1 + 1):
for z in range(z0, z1 + 1):
if x in (x0, x1) or z in (z0, z1):
self.put(x, y, z, self.rng.choice(pal))
for y in [4, 10] + ([16, 22] if top == 22 else []):
self.texture((x0, y, z0, x1, y, z1), TIMBER)
# Continuous dark perimeter beams.
for z in (z0, z1):
self.box((x0, y, z, x1, y, z), 'minecraft:dark_oak_log', axis='x')
for x in (x0, x1):
self.box((x, y, z0, x, y, z1), 'minecraft:dark_oak_log', axis='z')
for x in range(x0, x1 + 1, 4):
for z in (z0, z1):
self.box((x, 11, z, x, top - 1, z), 'minecraft:dark_oak_log', axis='y')
for z in range(z0, z1 + 1, 4):
for x in (x0, x1):
self.box((x, 11, z, x, top - 1, z), 'minecraft:dark_oak_log', axis='y')
for x, z in [(x0, z0), (x1, z0), (x0, z1), (x1, z1)]:
self.box((x, 5, z, x, 9, z), 'minecraft:polished_andesite')
# Tall roof has a two-block vertical rise per horizontal run.
for x in range(6, 25):
h = 38 - 2 * abs(x - 15)
for z in range(8, 31):
for y in range(h, min(h + 2, 40)):
self.put(x, y, z, self.rng.choice(ROOF))
if x != 15:
self.put(x, h + 1, z, 'minecraft:deepslate_tile_stairs', facing='east' if x < 15 else 'west', half='bottom', shape='straight', waterlogged='false')
for z in (8, 30):
self.box((x, h, z, x, min(h + 1, 39), z), 'minecraft:polished_deepslate')
for z in (10, 28):
if 7 <= x <= 23:
for y in range(23, h):
self.put(x, y, z, self.rng.choice(PLASTER))
if h > 23:
self.put(x, h - 1, z, 'minecraft:dark_oak_log', axis='y')
# The perpendicular lower roof intersects the tall roof and its side wall.
for z in range(11, 30):
h = 25 - abs(z - 20)
for x in range(23, 39):
self.put(x, h, z, self.rng.choice(ROOF))
self.put(x, h + 1, z, 'minecraft:deepslate_tile_stairs', facing='south' if z < 20 else 'north', half='bottom', shape='straight', waterlogged='false')
for x in (23, 37):
for y in range(16, h):
self.put(x, y, z, self.rng.choice(PLASTER) if x == 37 else 'minecraft:spruce_planks')
self.put(x, h, z, 'minecraft:dark_oak_log', axis='x')
self.box((23, 26, 20, 38, 26, 20), 'minecraft:deepslate_tile_slab', type='bottom', waterlogged='false')
for z in (10, 28):
self.box((15, 23, z, 15, 37, z), 'minecraft:dark_oak_log', axis='y')
self.box((10, 27, z, 20, 27, z), 'minecraft:dark_oak_log', axis='x')
self.box((12, 31, z, 18, 31, z), 'minecraft:dark_oak_log', axis='x')
for x in (11, 19):
self.box((x, 23, z, x, 29, z), 'minecraft:dark_oak_log', axis='y')
self.box((37, 16, 12, 37, 16, 28), 'minecraft:dark_oak_log', axis='z')
self.box((37, 17, 20, 37, 24, 20), 'minecraft:dark_oak_log', axis='y')
# Continuous flashings where the lower roof reaches the tall wing.
for z in range(12, 29):
h = 25 - abs(z - 20)
self.put(24, h, z, 'minecraft:polished_deepslate')
# Two storeys communicate through generous openings in the shared wall.
for y in (5, 11):
self.box((23, y, 18, 23, y + 2, 20), 'minecraft:air')
self.box((23, y + 3, 17, 23, y + 3, 21), 'minecraft:dark_oak_log', axis='z')
self.door(15, 5, 10, 'north')
self.door(16, 5, 10, 'north', 'right')
self.door(29, 5, 12, 'north')
for x in (14, 17):
self.box((x, 5, 9, x, 7, 9), 'minecraft:stripped_spruce_log', axis='y')
self.box((14, 8, 9, 17, 8, 9), 'minecraft:dark_oak_log', axis='x')
for x in (13,):
self.put(x, 7, 9, 'minecraft:dark_oak_log', axis='z')
self.lamp(x, 6, 9, True)
self.box((13, 9, 8, 18, 9, 9), 'minecraft:deepslate_tile_slab', type='bottom', waterlogged='false')
# Ground and upper windows retain a full clear block on both sides.
for a, b in [(9, 11), (19, 21)]:
self.window('x', 10, a, b, 6, 7, 'north')
for bottom in (12, 18):
for a, b in [(10, 12), (18, 20)]:
self.window('x', 10, a, b, bottom, bottom + 2, 'north')
self.window('x', 10, 13, 17, 25, 28, 'north')
self.window('x', 28, 13, 17, 25, 28, 'south')
for bottom in (6, 12, 18):
self.window('z', 7, 14, 16, bottom, bottom + 1, 'west')
self.window('x', 28, 9, 11, bottom, bottom + 1, 'south')
self.window('x', 12, 26, 28, 12, 14, 'north')
self.window('x', 28, 27, 29, 6, 7, 'south')
self.window('x', 28, 27, 29, 12, 14, 'south')
self.window('z', 37, 16, 18, 6, 7, 'east')
self.window('z', 37, 16, 18, 12, 14, 'east')
self.window('z', 37, 19, 21, 19, 21, 'east')
for z in (12, 23):
self.put(6, 8, z, 'minecraft:dark_oak_log', axis='x')
self.lamp(6, 7, z, True)
# Upright beam feet and exposed floor joists reinforce the timber silhouette.
for y in (10, 16, 22):
for x in (7, 11, 19, 23):
self.put(x, y, 9, 'minecraft:dark_oak_log', axis='z')
# Exterior vines have explicit faces against stone, and never cover glazing.
for y, zs in [(5, (19, 20, 21)), (6, (19, 20)), (7, (20,)), (8, (20, 21))]:
for z in zs:
self.put(6, y, z, 'minecraft:vine', east='true', west='false', north='false', south='false', up='false')
for y, xs in [(5, (31, 32, 33)), (6, (32, 33)), (7, (33,)), (8, (33, 34))]:
for x in xs:
self.put(x, y, 29, 'minecraft:vine', north='true', east='false', south='false', west='false', up='false')
def circulation(self) -> None:
"""Connect four floors with a supported ladder and the lower floors with a wide stair."""
self.box((21, 5, 26, 21, 25, 26), 'minecraft:stripped_spruce_log', axis='y')
for y in range(5, 24):
self.put(21, y, 25, 'minecraft:ladder', facing='north', waterlogged='false')
for y in (10, 16, 22):
self.put(21, y + 1, 24, 'minecraft:spruce_fence', north='false', south='true', east='false', west='false', waterlogged='false')
self.put(22, y + 1, 25, 'minecraft:spruce_fence', north='false', south='false', east='false', west='true', waterlogged='false')
self.box((35, 5, 27, 35, 13, 27), 'minecraft:stripped_spruce_log', axis='y')
for y in range(5, 12):
self.put(35, y, 26, 'minecraft:ladder', facing='north', waterlogged='false')
# Six treads rise from the hall to the living room, supported by a timber stringer.
for index in range(6):
y, z = 5 + index, 20 + index
self.box((9, 5, z, 10, y, z), 'minecraft:spruce_planks')
self.box((9, y, z, 10, y, z), 'minecraft:spruce_stairs', facing='south', half='bottom', shape='straight', waterlogged='false')
self.box((9, y + 1, z, 10, y + 3, z), 'minecraft:air')
# Railing follows the side of the opening, leaving a broad top landing.
for z in range(23, 26):
self.put(11, 11, z, 'minecraft:spruce_fence', north='true', south='true', east='false', west='false', waterlogged='false')
def room_details(self, anchor: Position, facing: str, item: str, pot: str, carpet: str) -> None:
"""Furnish a wall alcove with storage, a seat, textiles, and small objects.
Args:
anchor (Position): First cabinet at floor air height.
facing (str): Displayed item direction.
item (str): Item frame content.
pot (str): Potted plant block name.
carpet (str): Carpet color.
"""
x, y, z = anchor
self.storage(x, y, z, 'barrel', facing, item)
self.storage(x + 2, y, z, 'chest', facing, item)
self.put(x + 4, y, z, 'minecraft:spruce_planks')
self.put(x + 4, y + 1, z, 'minecraft:potted_' + pot)
self.put(x + 6, y, z, 'minecraft:spruce_planks')
self.put(x + 6, y + 1, z, 'minecraft:candle', candles='3', lit='true', waterlogged='false')
self.lamp(x, y + 1, z)
self.put(x + 2, y + 2, z, 'minecraft:dark_oak_planks')
dx, dz = DIRECTIONS[facing]
self.frame((x + 2 + dx, y + 2, z + dz), facing, item)
# Wall hung block has an actual timber hanger into the wall behind it.
self.put(x + 2 - dx, y + 2, z - dz, 'minecraft:dark_oak_log', axis='z')
for ox in range(3):
self.put(x + 3 + ox, y, z + (-2 if facing == 'north' else 2), 'minecraft:' + carpet + '_carpet')
self.put(x + 6, y, z + (-2 if facing == 'north' else 2), 'minecraft:spruce_stairs', facing='south' if facing == 'north' else 'north', half='bottom', shape='straight', waterlogged='false')
def furnish(self) -> None:
"""Give each of the six room groups a distinct, fully supplied interior."""
# Entrance hall and storage: clear central passage from the double front door.
self.room_details((13, 5, 27), 'north', 'iron_pickaxe', 'fern', 'gray')
self.box((8, 5, 12, 8, 6, 12), 'minecraft:bookshelf')
self.put(8, 5, 18, 'minecraft:crafting_table')
self.lamp(8, 6, 18)
self.box((14, 5, 14, 16, 5, 18), 'minecraft:brown_carpet')
self.put(8, 5, 14, 'minecraft:spruce_stairs', facing='east', half='bottom', shape='straight', waterlogged='false')
# Lower workshop with tool chest, grindstone, and repair counter.
self.room_details((25, 5, 25), 'north', 'iron_ingot', 'dead_bush', 'brown')
self.box((25, 5, 26, 31, 8, 26), 'minecraft:spruce_planks')
self.put(25, 5, 14, 'minecraft:crafting_table')
self.put(26, 5, 14, 'minecraft:smithing_table')
self.put(27, 5, 14, 'minecraft:stonecutter', facing='north')
self.put(33, 5, 15, 'minecraft:polished_andesite')
self.put(33, 6, 15, 'minecraft:grindstone', face='floor', facing='north')
self.put(31, 5, 17, 'minecraft:anvil', facing='east')
self.box((29, 5, 20, 31, 5, 22), 'minecraft:gray_carpet')
# Living room: fireplace, couches and a low central table.
self.room_details((13, 11, 27), 'north', 'book', 'azalea_bush', 'orange')
self.box((8, 11, 18, 8, 13, 20), 'minecraft:bookshelf')
self.box((12, 11, 15, 16, 11, 19), 'minecraft:orange_carpet')
for z in range(16, 19):
self.put(11, 11, z, 'minecraft:spruce_stairs', facing='east', half='bottom', shape='straight', waterlogged='false')
self.put(18, 11, z, 'minecraft:spruce_stairs', facing='west', half='bottom', shape='straight', waterlogged='false')
for z in (16, 18):
self.put(14, 11, z, 'minecraft:stripped_spruce_log', axis='y')
self.box((14, 12, 16, 14, 12, 18), 'minecraft:spruce_slab', type='bottom', waterlogged='false')
self.box((18, 11, 12, 21, 13, 13), 'minecraft:stone_bricks')
self.put(19, 11, 12, 'minecraft:campfire', lit='true', signal_fire='false', facing='north', waterlogged='false')
self.put(20, 11, 12, 'minecraft:campfire', lit='true', signal_fire='false', facing='north', waterlogged='false')
self.box((18, 14, 12, 21, 14, 13), 'minecraft:polished_andesite')
# Dining room: vaulted ceiling, long table and pantry.
self.room_details((25, 11, 25), 'north', 'apple', 'dandelion', 'red')
self.box((25, 11, 26, 31, 14, 26), 'minecraft:spruce_planks')
self.box((27, 11, 16, 33, 11, 22), 'minecraft:red_carpet')
for z in range(17, 22):
self.put(30, 11, z, 'minecraft:stripped_spruce_log', axis='y')
self.put(30, 12, z, 'minecraft:spruce_slab', type='top', waterlogged='false')
self.put(30, 13, 19, 'minecraft:green_candle', candles='3', lit='true', waterlogged='false')
for z in (17, 19, 21):
self.put(28, 11, z, 'minecraft:spruce_stairs', facing='east', half='bottom', shape='straight', waterlogged='false')
self.put(32, 11, z, 'minecraft:spruce_stairs', facing='west', half='bottom', shape='straight', waterlogged='false')
self.box((25, 11, 14, 25, 12, 16), 'minecraft:bookshelf')
for z in (15, 24):
self.put(34, 13, z, 'minecraft:dark_oak_log', axis='y')
self.box((34, 14, z, 34, 25 - abs(z - 20) - 1, z), 'minecraft:chain', axis='y', waterlogged='false')
self.lamp(34, 12, z, True)
# Bedroom: green double bed on a one-block dais with a real stair approach.
self.room_details((13, 17, 27), 'north', 'emerald', 'blue_orchid', 'green')
self.box((9, 17, 12, 13, 17, 15), 'minecraft:spruce_planks')
for x in range(9, 14):
self.put(x, 17, 16, 'minecraft:spruce_stairs', facing='north', half='bottom', shape='straight', waterlogged='false')
self.box((9, 18, 12, 13, 19, 12), 'minecraft:dark_oak_planks')
for x in (10, 11):
self.put(x, 18, 13, 'minecraft:green_bed', facing='north', part='head', occupied='false')
self.put(x, 18, 14, 'minecraft:green_bed', facing='north', part='foot', occupied='false')
self.lamp(13, 18, 14)
self.box((14, 17, 17, 18, 17, 20), 'minecraft:green_carpet')
self.box((8, 17, 19, 8, 20, 22), 'minecraft:bookshelf')
for x in (18, 20):
self.storage(x, 20, 27, 'barrel', 'north', 'paper')
self.put(x, 20, 28, 'minecraft:dark_oak_log', axis='z')
# Paired curtains hang beside the bedroom's front-right window.
for x in (17, 21):
self.put(x, 20, 11, 'minecraft:green_wall_banner', facing='south')
self.block_entity((x, 20, 11), 'banner', patterns=NBTList[Compound]([Compound({'pattern': String('minecraft:small_stripes'), 'color': String('white')}), Compound({'pattern': String('minecraft:border'), 'color': String('green')})]))
# Attic study: books, writing desk and a lit reading nook below the rafters.
self.room_details((10, 23, 26), 'north', 'map', 'red_tulip', 'cyan')
self.box((10, 23, 27, 16, 24, 27), 'minecraft:spruce_planks')
self.box((10, 23, 18, 10, 25, 22), 'minecraft:bookshelf')
self.box((14, 23, 13, 17, 23, 13), 'minecraft:spruce_planks')
self.put(14, 24, 13, 'minecraft:potted_fern')
self.lamp(17, 24, 13)
self.put(15, 23, 15, 'minecraft:spruce_stairs', facing='north', half='bottom', shape='straight', waterlogged='false')
self.put(18, 23, 15, 'minecraft:lectern', facing='west', has_book='true', powered='false')
self.block_entity((18, 23, 15), 'lectern', Book=Compound({'id': String('minecraft:written_book'), 'count': Int(1), 'components': Compound({'minecraft:written_book_content': Compound({'title': Compound({'raw': String('Amberstone field notes')}), 'author': String('The Lodge Keeper'), 'pages': NBTList[Compound]([Compound({'raw': String('{"text":"Amberstone Lodge\\n\\nThe forge burns below. Pine shadows cross the terrace.\\n\\nRoof survey: autumn, year 21."}')})]), 'resolved': Byte(1), 'generation': Int(0)})})}), Page=Int(0))
self.box((13, 23, 18, 17, 23, 22), 'minecraft:cyan_carpet')
for z in (15, 23):
# Short tie beams touch both roof slopes; pendants hang low enough to light the floor.
self.box((10, 28, z, 20, 28, z), 'minecraft:dark_oak_log', axis='x')
self.box((15, 26, z, 15, 27, z), 'minecraft:chain', axis='y', waterlogged='false')
self.lamp(15, 25, z, True)
def smithy(self) -> None:
"""Build the sheltered forge, lit hearth, chimney, anvil and supply bins."""
for x in (25, 37):
self.box((x, 5, 7, x, 9, 7), 'minecraft:dark_oak_log', axis='y')
self.put(x, 5, 7, 'minecraft:polished_andesite')
self.box((x, 5, 11, x, 12, 11), 'minecraft:dark_oak_log', axis='y')
self.box((25, 9, 7, 37, 9, 7), 'minecraft:dark_oak_log', axis='x')
for z in range(6, 12):
y = 9 + (z - 6) // 2
for x in range(24, 39):
self.put(x, y, z, self.rng.choice(ROOF))
if z % 2 == 0:
self.put(x, y, z, 'minecraft:deepslate_tile_stairs', facing='south', half='bottom', shape='straight', waterlogged='false')
for x in (26, 31, 36):
self.lamp(x, 8, 7, True)
self.texture((33, 5, 9, 36, 5, 11), ['minecraft:deepslate_bricks', 'minecraft:polished_deepslate'])
for x in (33, 36):
self.texture((x, 6, 9, x, 8, 11), STONE)
self.texture((33, 6, 11, 36, 8, 11), STONE)
self.texture((33, 8, 9, 36, 8, 11), STONE)
for x in (34, 35):
self.put(x, 5, 9, 'minecraft:blast_furnace', facing='north', lit='true')
self.block_entity((x, 5, 9), 'blast_furnace', BurnTime=Short(16000), CookTime=Short(0), CookTimeTotal=Short(100), Items=NBTList[Compound]([Compound({'Slot': Byte(0), 'id': String('minecraft:raw_iron'), 'count': Int(64)}), Compound({'Slot': Byte(1), 'id': String('minecraft:coal_block'), 'count': Int(64)})]))
self.put(x, 6, 10, 'minecraft:campfire', lit='true', signal_fire='false', facing='north', waterlogged='false')
self.texture((34, 9, 10, 35, 26, 11), ['minecraft:stone_bricks'] * 5 + ['minecraft:bricks', 'minecraft:cracked_stone_bricks'])
for y in (14, 20, 25):
self.box((34, y, 10, 35, y, 11), 'minecraft:polished_andesite')
self.box((33, 27, 9, 36, 27, 12), 'minecraft:stone_brick_slab', type='bottom', waterlogged='false')
self.put(31, 5, 8, 'minecraft:anvil', facing='east')
self.storage(26, 5, 10, item='iron_ingot')
self.storage(27, 5, 10, 'chest', item='coal')
self.put(26, 6, 10, 'minecraft:potted_fern')
self.put(28, 5, 10, 'minecraft:smithing_table')
self.put(31, 5, 10, 'minecraft:water_cauldron', level='3')
self.frame((32, 7, 11), 'north', 'iron_axe')
# Forge sign uses the solid front wall as its backing.
self.put(32, 7, 12, 'minecraft:dark_oak_planks')
def pine(self, cx: int, cz: int, height: int, radius: int) -> None:
"""Grow a handmade pine with an exposed trunk and stepped, layered boughs.
Args:
cx (int): Trunk X coordinate.
cz (int): Trunk Z coordinate.
height (int): Height above terrain.
radius (int): Maximum bough radius.
"""
self.box((cx, 2, cz, cx, height + 1, cz), 'minecraft:spruce_log', axis='y')
for y in range(5, height + 1, 3):
reach = max(1, round(radius * (height + 3 - y) / height))
for dx, dz in DIRECTIONS.values():
for step in range(1, reach + 1):
py = y + (1 if step == reach and reach > 1 else 0)
x, z = cx + dx * step, cz + dz * step
if self.inside(x, py, z) and not self.get(x, py, z):
self.put(x, py, z, 'minecraft:spruce_log', axis='x' if dx else 'z')
for dy, r in [(0, reach), (1, reach), (2, max(0, reach - 1))]:
for dx in range(-r, r + 1):
for dz in range(-r, r + 1):
x, z, py = cx + dx, cz + dz, y + dy
if abs(dx) + abs(dz) > r + max(1, r // 2):
continue
if not self.inside(x, py, z) or self.get(x, py, z):
continue
leaf = 'minecraft:spruce_leaves' if self.rng.random() < .86 else 'minecraft:oak_leaves'
self.put(x, py, z, leaf, persistent='true', distance='1', waterlogged='false')
for dy in (1, 2):
self.put(cx, height + 1 + dy, cz, 'minecraft:spruce_leaves', persistent='true', distance='1', waterlogged='false')
def garden(self) -> None:
"""Add pines, planted beds, supported shrubs and scattered stones."""
self.pine(3, 25, 22, 4)
self.pine(41, 28, 20, 4)
self.pine(41, 5, 13, 3)
for x, z in [(3, 17), (4, 19), (40, 17), (41, 19), (9, 32), (27, 32), (30, 3)]:
self.put(x, 2, z, 'minecraft:mossy_cobblestone')
self.put(x, 3, z, 'minecraft:azalea_leaves', persistent='true', distance='1', waterlogged='false')
for x, z in [(8, 3), (10, 2), (22, 3), (26, 4), (33, 3), (4, 13), (3, 30), (13, 32), (21, 32), (34, 32), (42, 13)]:
self.put(x, 2, z, 'minecraft:fern')
for x, z in [(7, 4), (24, 3), (29, 4), (11, 32), (32, 32), (41, 15)]:
self.put(x, 2, z, self.rng.choice(['minecraft:poppy', 'minecraft:dandelion', 'minecraft:azure_bluet']))
for x in (8, 22):
self.put(x, 5, 8, 'minecraft:coarse_dirt')
self.put(x, 6, 8, 'minecraft:flowering_azalea')
self.put(x, 5, 7, 'minecraft:spruce_trapdoor', facing='north', open='true', half='bottom', powered='false', waterlogged='false')
for x, z in [(5, 2), (35, 3), (39, 21), (6, 32)]:
self.put(x, 2, z, 'minecraft:mossy_cobblestone')
self.put(x + 1, 2, z, 'minecraft:andesite')
def build(self) -> 'Build':
"""Construct Amberstone Lodge and all landscape and furnishing details.
Returns:
Build: The completed instance.
"""
self.landscape()
self.architecture()
self.circulation()
self.furnish()
self.smithy()
self.garden()
return self
def export(self, path: Path) -> Path:
"""Write the schematic so that two runs produce identical bytes.
Args:
path (Path): Destination .litematic path.
Returns:
Path: The path written.
"""
schematic = self.canvas.to_litematica(
name=CONFIG["name"],
author=CONFIG["author"],
description=CONFIG["description"],
minecraft_data_version=CONFIG["data_version"],
)
# The metadata otherwise records the current wall clock, which changes the bytes on every run.
schematic.metadata.time_created = 0
schematic.metadata.time_modified = 0
path.parent.mkdir(parents=True, exist_ok=True)
nbt = schematic.write_to_nbt()
region = nbt['Regions'][CONFIG['name']]
region['TileEntities'] = NBTList[Compound](self.tiles)
region['Entities'] = NBTList[Compound](self.entities)
# Standard compressed NBT with a fixed gzip timestamp preserves byte reproducibility.
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],
"non_air_blocks": sum(state != 'minecraft:air' for state in self.placed.values()),
"block_entities": len(self.tiles),
"item_frames": len(self.entities),
"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()