```python
import bpy, random, math
from mathutils import Vector

random.seed(42)

# ---------- Cleanup ----------
for o in list(bpy.data.objects):
    bpy.data.objects.remove(o, do_unlink=True)
for coll in [bpy.data.meshes, bpy.data.materials, bpy.data.worlds]:
    for b in list(coll):
        try:
            coll.remove(b)
        except Exception:
            pass

scene = bpy.context.scene
scene.render.engine = 'CYCLES'
try:
    scene.cycles.device = 'GPU'
except Exception:
    pass
scene.cycles.samples = 64
scene.view_settings.view_transform = 'AgX'

# ---------- World ----------
world = bpy.data.worlds.new("World")
scene.world = world
world.use_nodes = True
bg = world.node_tree.nodes["Background"]
bg.inputs[0].default_value = (0.55, 0.62, 0.65, 1)
bg.inputs[1].default_value = 1.1

def new_mat(name):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    return m

def link(mat, out, inp):
    mat.node_tree.links.new(out, inp)

# ---------- Materials ----------
def make_wall_mat():
    m = new_mat("Wall")
    nt = m.node_tree
    nodes = nt.nodes
    nodes.clear()
    out = nodes.new("ShaderNodeOutputMaterial")
    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.inputs["Roughness"].default_value = 0.9
    noise = nodes.new("ShaderNodeTexNoise")
    noise.inputs["Scale"].default_value = 15.0
    noise.inputs["Detail"].default_value = 8.0
    colorramp = nodes.new("ShaderNodeValToRGB")
    colorramp.color_ramp.elements[0].position = 0.35
    colorramp.color_ramp.elements[0].color = (0.25,0.22,0.18,1)
    colorramp.color_ramp.elements[1].position = 0.65
    colorramp.color_ramp.elements[1].color = (0.55,0.5,0.42,1)
    mossnoise = nodes.new("ShaderNodeTexNoise")
    mossnoise.inputs["Scale"].default_value = 4.0
    mossramp = nodes.new("ShaderNodeValToRGB")
    mossramp.color_ramp.elements[0].position = 0.45
    mossramp.color_ramp.elements[0].color = (0,0,0,1)
    mossramp.color_ramp.elements[1].position = 0.65
    mossramp.color_ramp.elements[1].color = (1,1,1,1)
    mossgreen = nodes.new("ShaderNodeRGB")
    mossgreen.outputs[0].default_value = (0.13,0.28,0.08,1)
    mixcolor = nodes.new("ShaderNodeMixRGB")
    mixcolor.inputs["Fac"].default_value = 0.0
    bump = nodes.new("ShaderNodeBump")
    bump.inputs["Strength"].default_value = 0.4
    link(m, noise.outputs["Fac"], colorramp.inputs["Fac"])
    link(m, colorramp.outputs["Color"], mixcolor.inputs["Color1"])
    link(m, mossgreen.outputs[0], mixcolor.inputs["Color2"])
    link(m, mossnoise.outputs["Fac"], mossramp.inputs["Fac"])
    link(m, mossramp.outputs["Color"], mixcolor.inputs["Fac"])
    link(m, mixcolor.outputs["Color"], bsdf.inputs["Base Color"])
    link(m, noise.outputs["Fac"], bump.inputs["Height"])
    link(m, bump.outputs["Normal"], bsdf.inputs["Normal"])
    link(m, bsdf.outputs["BSDF"], out.inputs["Surface"])
    return m

def make_roof_mat():
    m = new_mat("RustedRoof")
    nt = m.node_tree
    nodes = nt.nodes
    nodes.clear()
    out = nodes.new("ShaderNodeOutputMaterial")
    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.inputs["Roughness"].default_value = 0.55
    bsdf.inputs["Metallic"].default_value = 0.6
    wave = nodes.new("ShaderNodeTexWave")
    wave.inputs["Scale"].default_value = 20.0
    wave.bands_direction = 'X'
    noise = nodes.new("ShaderNodeTexNoise")
    noise.inputs["Scale"].default_value = 30.0
    noise.inputs["Detail"].default_value = 10.0
    ramp = nodes.new("ShaderNodeValToRGB")
    ramp.color_ramp.elements[0].position = 0.3
    ramp.color_ramp.elements[0].color = (0.15,0.05,0.02,1)
    ramp.color_ramp.elements[1].position = 0.7
    ramp.color_ramp.elements[1].color = (0.55,0.22,0.08,1)
    mixmetal = nodes.new("ShaderNodeMixRGB")
    mixmetal.inputs["Color2"].default_value = (0.4,0.4,0.42,1)
    mixmetal.inputs["Fac"].default_value = 0.4
    bump = nodes.new("ShaderNodeBump")
    bump.inputs["Strength"].default_value = 0.6
    link(m, noise.outputs["Fac"], ramp.inputs["Fac"])
    link(m, ramp.outputs["Color"], mixmetal.inputs["Color1"])
    link(m, mixmetal.outputs["Color"], bsdf.inputs["Base Color"])
    link(m, wave.outputs["Fac"], bump.inputs["Height"])
    link(m, bump.outputs["Normal"], bsdf.inputs["Normal"])
    link(m, ramp.outputs["Color"], bsdf.inputs["Roughness"])
    link(m, bsdf.outputs["BSDF"], out.inputs["Surface"])
    return m

def make_ground_mat():
    m = new_mat("Ground")
    nt = m.node_tree
    nodes = nt.nodes
    nodes.clear()
    out = nodes.new("ShaderNodeOutputMaterial")
    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.inputs["Roughness"].default_value = 0.95
    noise = nodes.new("ShaderNodeTexNoise")
    noise.inputs["Scale"].default_value = 8.0
    noise.inputs["Detail"].default_value = 10.0
    ramp = nodes.new("ShaderNodeValToRGB")
    ramp.color_ramp.elements[0].position = 0.3
    ramp.color_ramp.elements[0].color = (0.08,0.16,0.04,1)
    ramp.color_ramp.elements[1].position = 0.7
    ramp.color_ramp.elements[1].color = (0.2,0.32,0.09,1)
    bump = nodes.new("ShaderNodeBump")
    bump.inputs["Strength"].default_value = 0.5
    link(m, noise.outputs["Fac"], ramp.inputs["Fac"])
    link(m, ramp.outputs["Color"], bsdf.inputs["Base Color"])
    link(m, noise.outputs["Fac"], bump.inputs["Height"])
    link(m, bump.outputs["Normal"], bsdf.inputs["Normal"])
    link(m, bsdf.outputs["BSDF"], out.inputs["Surface"])
    return m

def make_leaf_mat(dark=False):
    m = new_mat("Leaves" if not dark else "LeavesDark")
    nt = m.node_tree
    nodes = nt.nodes
    nodes.clear()
    out = nodes.new("ShaderNodeOutputMaterial")
    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.inputs["Roughness"].default_value = 0.7
    base = (0.08,0.25,0.05,1) if not dark else (0.05,0.15,0.03,1)
    bsdf.inputs["Base Color"].default_value = base
    link(m, bsdf.outputs["BSDF"], out.inputs["Surface"])
    return m

def make_vine_mat():
    m = new_mat("VineStem")
    nt = m.node_tree
    nodes = nt.nodes
    nodes.clear()
    out = nodes.new("ShaderNodeOutputMaterial")
    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.inputs["Base Color"].default_value = (0.15,0.12,0.06,1)
    bsdf.inputs["Roughness"].default_value = 0.85
    link(m, bsdf.outputs["BSDF"], out.inputs["Surface"])
    return m

def make_wood_mat():
    m = new_mat("WoodDoor")
    nt = m.node_tree
    nodes = nt.nodes
    nodes.clear()
    out = nodes.new("ShaderNodeOutputMaterial")
    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.inputs["Base Color"].default_value = (0.12,0.08,0.05,1)
    bsdf.inputs["Roughness"].default_value = 0.9
    link(m, bsdf.outputs["BSDF"], out.inputs["Surface"])
    return m

wall_mat = make_wall_mat()
roof_mat = make_roof_mat()
ground_mat = make_ground_mat()
leaf_mat = make_leaf_mat(False)
leaf_mat_dark = make_leaf_mat(True)
vine_mat = make_vine_mat()
wood_mat = make_wood_mat()

# ---------- Ground ----------
bpy.ops.mesh.primitive_plane_add(size=60, location=(0,0,0))
ground = bpy.context.object
ground.name = "Ground"
ground.data.materials.append(ground_mat)
mod = ground.modifiers.new("Subsurf", 'SUBSURF')
mod.levels = 3
mod.render_levels = 3
disp = ground.modifiers.new("Disp", 'DISPLACE')
tex = bpy.data.textures.new("GroundDisp", 'CLOUDS')
tex.noise_scale = 3.0
disp.texture = tex
disp.strength = 0.35

# ---------- House builder ----------
def build_house(loc, rot_z, w, d, h, tilt=0.0):
    grp = []
    bpy.ops.mesh.primitive_cube_add(size=1, location=(loc[0], loc[1], loc[2]+h/2))
    body = bpy.context.object
    body.name = "HouseBody"
    body.scale = (w/2, d/2, h/2)
    body.rotation_euler[2] = rot_z
    body.rotation_euler[0] = tilt
    body.data.materials.append(wall_mat)
    bpy.ops.object.shade_smooth()
    grp.append(body)

    # cut some window/door holes via simple boolean
    def cut_hole(px, py, pz, sx, sy, sz):
        bpy.ops.mesh.primitive_cube_add(size=1, location=(px,py,pz))
        cutter = bpy.context.object
        cutter.scale = (sx,sy,sz)
        m = body.modifiers.new("Bool", 'BOOLEAN')
        m.operation = 'DIFFERENCE'
        m.object = cutter
        bpy.context.view_layer.objects.active = body
        bpy.ops.object.modifier_apply(modifier=m.name)
        bpy.data.objects.remove(cutter, do_unlink=True)

    local_positions = [(0.0, d/2, h*0.55), (0.0, -d/2, h*0.55)]
    for lp in local_positions:
        wx = loc[0] + math.cos(rot_z)*lp[0] - math.sin(rot_z)*lp[1]
        wy = loc[1] + math.sin(rot_z)*lp[0] + math.cos(rot_z)*lp[1]
        cut_hole(wx, wy, loc[2]+lp[2], w*0.3, d*0.4, h*0.25)

    # door
    dx = loc[0] + math.cos(rot_z)*0 - math.sin(rot_z)*(-d/2)
    dy = loc[1] + math.sin(rot_z)*0 + math.cos(rot_z)*(-d/2)
    cut_hole(dx, dy, loc[2]+h*0.2, w*0.18, d*0.4, h*0.4)

    # roof - a flattened, rusted slanted box
    roof_h = h*0.28
    bpy.ops.mesh.primitive_cube_add(size=1, location=(loc[0], loc[1], loc[2]+h+roof_h/2))
    roof = bpy.context.object
    roof.name = "Roof"
    roof.scale = (w/2*1.15, d/2*1.15, roof_h/2)
    roof.rotation_euler[2] = rot_z
    roof.rotation_euler[0] = tilt*1.5 + random.uniform(-0.03,0.03)
    roof.data.materials.append(roof_mat)
    bpy.ops.object.shade_smooth()
    # slight collapse on some roofs to sell decay
    grp.append(roof)

    return grp, (loc[0], loc[1], loc[2]+h)

house_data = [
    ((-6, -4, 0), 0.05, 4.5, 4.0, 3.6, 0.02),
    ((5, -3, 0), -0.3, 3.8, 3.6, 3.2, -0.05),
    ((-1, 6, 0), 0.15, 5.2, 4.2, 4.0, 0.0),
    ((7, 5, 0), 0.4, 3.5, 3.2, 2.8, 0.06),
]
roof_tops = []
for loc, rot, w, d, h, tilt in house_data:
    grp, top = build_house(loc, rot, w, d, h, tilt)
    roof_tops.append((top, w, d, rot))

# ---------- Foliage helpers ----------
def make_leaf_blob(loc, radius, mat, subdiv=2):
    bpy.ops.mesh.primitive_ico_sphere_add(radius=radius, location=loc, subdivisions=subdiv)
    ob = bpy.context.object
    ob.scale = (1+random.uniform(-0.2,0.3), 1+random.uniform(-0.2,0.3), 0.6+random.uniform(-0.1,0.2))
    ob.rotation_euler = (random.uniform(0,3.14), random.uniform(0,3.14), random.uniform(0,3.14))
    disp = ob.modifiers.new("Disp", 'DISPLACE')
    tex = bpy.data.textures.new("LeafTex", 'CLOUDS')
    tex.noise_scale = 0.5
    disp.texture = tex
    disp.strength = radius*0.4
    ob.data.materials.append(mat)
    bpy.ops.object.shade_smooth()
    return ob

# scattered bushes / overgrowth around bases and roofs
for loc, rot, w, d, h, tilt in house_data:
    for i in range(random.randint(4,7)):
        ang = random.uniform(0,2*3.14159)
        dist = random.uniform(w*0.4, w*0.9)
        bx = loc[0] + math.cos(ang)*dist
        by = loc[1] + math.sin(ang)*dist
        make_leaf_blob((bx,by, random.uniform(0.3,0.9)), random.uniform(0.5,1.1),
                        random.choice([leaf_mat, leaf_mat_dark]))

# moss/leaf blobs on rooftops
for top, w, d, rot in roof_tops:
    for i in range(random.randint(3,6)):
        lx = random.uniform(-w*0.4, w*0.4)
        ly = random.uniform(-d*0.4, d*0.4)
        wx = top[0] + math.cos(rot)*lx - math.sin(rot)*ly
        wy = top[1] + math.sin(rot)*lx + math.cos(rot)*ly
        make_leaf_blob((wx, wy, top[2]+0.3), random.uniform(0.35,0.7), leaf_mat_dark, subdiv=1)

# ---------- Vines using curves ----------
def make_vine(base, height, mat, sway=0.4):
    curve = bpy.data.curves.new("Vine", 'CURVE')
    curve.dimensions = '3D'
    spline = curve.splines.new('NURBS')
    n_pts = 8
    spline.points.add(n_pts-1)
    for i in range(n_pts):
        t = i/(n_pts-1)
        x = base[0] + math.sin(t*4+random.uniform(0,3))*sway*t
        y = base[1] + math.cos(t*3+random.uniform(0,3))*sway*t*0.6
        z = base[2] + height*t
        spline.points[i].co = (x,y,z,1)
    spline.use_endpoint_u = True
    curve.bevel_depth = 0.03
    curve.bevel_resolution = 3
    ob = bpy.data.objects.new("VineObj", curve)
    bpy.context.collection.objects.link(ob)
    ob.data.materials.append(mat)
    # small leaves along vine
    for i in range(6):
        t = random.uniform(0.1,1.0)
        x = base[0] + math.sin(t*4)*sway*t
        y = base[1] + math.cos(t*3)*sway*t*0.6
        z = base[2] + height*t
        make_leaf_blob((x,y,z), random.uniform(0.15,0.3), leaf_mat, subdiv=1)
    return ob

for loc, rot, w, d, h, tilt in house_data:
    for i in range(random.randint(3,5)):
        wall_side = random.choice([1,-1])
        offset = random.uniform(-w*0.4, w*0.4)
        vx = loc[0] + math.cos(rot)*offset + math.sin(rot)*wall_side*d/2*0.98
        vy = loc[1] + math.sin(rot)*offset - math.cos(rot)*wall_side*d/2*0.98
        make_vine((vx, vy, 0.1), h*random.uniform(0.7,1.1), vine_mat)

# ---------- Grass field (particle-less, using mesh instancing via geometry-lite approach) ----------
grass_mat = new_mat("Grass")
nt = grass_mat.node_tree
nodes = nt.nodes
nodes.clear()
out = nodes.new("ShaderNodeOutputMaterial")
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.inputs["Base Color"].default_value = (0.12,0.3,0.06,1)
bsdf.inputs["Roughness"].default_value = 0.8
link(grass_mat, bsdf.outputs["BSDF"], out.inputs["Surface"])

bpy.ops.mesh.primitive_plane_add(size=1)
grass_blade = bpy.context.object
grass_blade.name = "GrassBlade"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
grass_blade.scale = (0.05, 0.4, 1)
grass_blade.data.materials.append(grass_mat)
grass_blade.hide_render = True
grass_blade.hide_viewport = True

# use particle system on ground for grass
psys = ground.modifiers.new("GrassParticles", 'PARTICLE_SYSTEM')
psettings = psys.particle_system.settings
psettings.type = 'HAIR'
psettings.count = 8000
psettings.hair_length = 0.35
psettings.render_type = 'OBJECT'
psettings.instance_object = grass_blade
psettings.use_advanced_hair = True
psettings.child_type = 'INTERPOLATED'
psettings.rendered_child_count = 20
psettings.roughness_endpoint = 0.3
psettings.roughness_1 = 0.3
psettings.particle_size = 0.6
psettings.size_random = 0.5

# ---------- Camera ----------
cam_data = bpy.data.cameras.new("Camera")
cam_data.lens = 32
cam = bpy.data.objects.new("Camera", cam_data)
bpy.context.collection.objects.link(cam)
cam.location = (13, -14, 8.5)
direction = Vector((0,1,0)) - cam.location
target = Vector((0,1,2))
look = target - cam.location
rot_quat = look.to_track_quat('-Z','Y')
cam.rotation_euler = rot_quat.to_euler()
scene.camera = cam

# ---------- Lighting ----------
sun_data = bpy.data.lights.new("Sun", 'SUN')
sun_data.energy = 3.2
sun_data.angle = 0.15
sun = bpy.data.objects.new("Sun", sun_data)
sun.rotation_euler = (math.radians(55), 0, math.radians(35))
bpy.context.collection.objects.link(sun)

fill_data = bpy.data.lights.new("Fill", 'AREA')
fill_data.energy = 200
fill_data.size = 10
fill = bpy.data.objects.new("Fill", fill_data)
fill.location = (-10, 8, 8)
fill.rotation_euler = (math.radians(60), 0, math.radians(-120))
bpy.context.collection.objects.link(fill)

# ---------- Save ----------
import os
outpath = os.path.join(os.getcwd(), "abandoned_village.blend")
try:
    bpy.ops.wm.save_as_mainfile(filepath=outpath)
except Exception as e:
    print("Save failed:", e)
```