raspberryjammod-minetest/raspberryjammod/mcpipy/danielbates_setblockdemo.py
2015-09-28 08:41:18 -05:00

439 lines
13 KiB
Python
Executable File

#!/usr/bin/env python
#
from math import sin, cos, radians
import danielbates_minecraft_basic as mc
#import pygame.image # comment this out if not using images - it's slow to import. If you uncomment, uncomment the image reference below.
import random
import server
# TODO: use numpy matrices/vectors instead of my own ones.
class coordinate3d:
"""Class used to represent a point in 3D space."""
def __init__(self,x,y,z):
self.x = x
self.y = y
self.z = z
def __add__(self, other):
return coordinate3d(self.x+other.x, self.y+other.y, self.z+other.z)
class transformation:
"""Representation of homogeneous matrices used to apply transformations to
coordinates - using a 4x4 matrix allows shifts as well as scales/rotations.
Transformations can be combined by multiplying them together."""
def __init__(self, matrix):
self.matrix = matrix
def __mul__(self, other):
if isinstance(other, transformation):
return self.compose(other)
elif isinstance(other, coordinate3d):
return self.apply(other)
else:
print "Can't multiply transformation by {0}".format(type(other))
def compose(self, other):
"""Compose this transformation with another, returning a new transformation."""
newmatrix = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]
for i in range(4):
for j in range(4):
for k in range(4):
newmatrix[i][k] += self.matrix[i][j]*other.matrix[j][k]
return transformation(newmatrix)
def apply(self, point):
"""Apply this transformation to a coordinate, returning a new coordinate."""
return coordinate3d(
self.matrix[0][0]*point.x + self.matrix[0][1]*point.y + self.matrix[0][2]*point.z + self.matrix[0][3],
self.matrix[1][0]*point.x + self.matrix[1][1]*point.y + self.matrix[1][2]*point.z + self.matrix[1][3],
self.matrix[2][0]*point.x + self.matrix[2][1]*point.y + self.matrix[2][2]*point.z + self.matrix[2][3])
## Shape functions
def cuboid(dx,dy,dz):
for x in range(dx):
for y in range(dy):
for z in range(dz):
yield coordinate3d(x,y,z)
def floor(dx,dz):
return cuboid(dx,1,dz)
def hollowcuboid(dx,dy,dz):
# Iterating through the six faces would be more efficient, but I'm lazy.
for x in range(dx):
for y in range(dy):
for z in range(dz):
if x==0 or x==(dx-1) or y==0 or y==(dy-1) or z==0 or z==(dz-1):
yield coordinate3d(x,y,z)
def sphere(r):
for x in range(-r,r):
for y in range(-r,r):
for z in range(-r,r):
if x**2 + y**2 + z**2 < r**2:
yield coordinate3d(x,y,z)
def pyramid(h):
for level in range(h):
for point in floor(2*(h-level),2*(h-level)):
yield point + coordinate3d(level,level,level)
def cylinder(r,h):
for x in range(-int(r),int(r)):
for z in range(-int(r),int(r)):
if x**2 + z**2 < r**2:
for y in range(h):
yield coordinate3d(x,y,z)
def cone(r,h):
for level in range(h):
for point in cylinder((float(h-level)/h)*r,1):
yield point + coordinate3d(0,level,0)
def line(x0,y0,z0,x1,y1,z1):
"""Draw a line using a 3D adaptation of Bressenham's algorithm.
http://www.cobrabytes.com/index.php?topic=1150.0"""
# Check for steep xy line
swap_xy = abs(y1-y0) > abs(x1-x0)
if swap_xy:
x0,y0 = y0,x0
x1,y1 = y1,x1
# Check for steep xz line
swap_xz = abs(z1-z0) > abs(x1-x0)
if swap_xz:
x0,z0 = z0,x0
x1,z1 = z1,x1
# Lengths in each direction
delta_x = abs(x1-x0)
delta_y = abs(y1-y0)
delta_z = abs(z1-z0)
# Drift tells us when to take a step in a direction
drift_xy = delta_x/2
drift_xz = delta_x/2
# Direction of line
step_x = 1
if x0 > x1: step_x = -1
step_y = 1
if y0 > y1: step_y = -1
step_z = 1
if z0 > z1: step_z = -1
# Starting point
y = y0
z = z0
for x in range(x0,x1,step_x):
cx,cy,cz = x,y,z
# Perform any necessary unswaps
if swap_xz: cx,cz = cz,cx
if swap_xy: cx,cy = cy,cx
# Place a block
yield coordinate3d(cx,cy,cz)
# Update progress
drift_xy -= delta_y
drift_xz -= delta_z
# Step in y direction
if drift_xy < 0:
y += step_y
drift_xy += delta_x
# Step in z direction
if drift_xz < 0:
z += step_z
drift_xz += delta_x
# Final block
yield coordinate3d(x1,y1,z1)
def text(data):
# Not implemented yet - create an image from the text, and search for coloured
# pixels.
pass
def mengersponge(depth):
"""3D cube-based fractal."""
if depth == 0:
yield coordinate3d(0,0,0)
else:
scale = 3**(depth-1) # size of each sub-cube
for x in range(3):
for y in range(3):
for z in range(3):
if not(x==1 and y==1 or x==1 and z==1 or y==1 and z==1):
for block in mengersponge(depth-1):
yield block + coordinate3d(x*scale,y*scale,z*scale)
def building(width, height, depth):
"""All dimensions are specified in the number of windows."""
for point in hollowcuboid(width*5-1, height*5+1, depth*5-1):
# Shift the building down by 1 so the floor is the right height.
yield point + coordinate3d(0,-1,0)
def revolvingdoor():
# A couple of shifts we need to get the doors to cross.
# This does work, but it was a bit too jerky to show off in the video.
xshift = shift(-2,0,0)
zshift = shift(0,0,-2)
for point in cuboid(1,3,5):
yield zshift*point
for point in cuboid(5,3,1):
yield xshift*point
def maze(width, depth):
"""Credit to autophil! http://jsfiddle.net/q7DSY/4/"""
# Ensure width and depth are odd so we get outer walls
if width%2==0: width += 1
if depth%2==0: depth += 1
maze.location = (1,1)
history = []
# Initialise 2d grid: 0 = wall; 1 = passageway.
grid = [depth*[0] for x in range(width)]
grid[maze.location[0]][maze.location[1]] = 1
history.append(maze.location)
def randomiseDirections():
directions = [(0,1),(1,0),(0,-1),(-1,0)]
random.shuffle(directions)
return directions
# Work out where to go next - don't want to leave the maze or go somewhere
# we've already been.
def nextDirection():
for direction in randomiseDirections():
x = maze.location[0] + 2*direction[0]
z = maze.location[1] + 2*direction[1]
if 0<x<width and 0<z<depth and grid[x][z]==0:
return direction
# Dig two squares or backtrack
def dig():
direction = nextDirection()
if direction:
for i in range(2):
maze.location = (maze.location[0] + direction[0], maze.location[1] + direction[1])
grid[maze.location[0]][maze.location[1]] = 1
history.append(maze.location)
return True
elif history:
maze.location = history.pop()
return maze.location
else:
return None
# Keep digging out the maze until we can't dig any more.
while dig():
pass
# Finally, start returning the blocks to draw.
for x in range(width):
for z in range(depth):
if grid[x][z] == 0:
yield coordinate3d(x,0,z)
yield coordinate3d(x,1,z)
yield coordinate3d(x,2,z)
arrow = [coordinate3d(0,0,0), coordinate3d(0,1,0), coordinate3d(0,2,0),
coordinate3d(0,3,0), coordinate3d(0,4,0), coordinate3d(-2,2,0),
coordinate3d(-1,3,0), coordinate3d(1,3,0), coordinate3d(2,2,0)]
## Fill functions
def solid(material):
"""All one material."""
def f(point):
return material
return f
def randomfill(materials):
"""Choose a random material from those listed. A material may be repeated to
increase its chance of being chosen."""
def f(point):
return random.choice(materials)
return f
def chequers(material1, material2):
"""Alternate between materials (in all directions)."""
def f(point):
if (point.x+point.y+point.z) % 2 == 0:
return material1
else:
return material2
return f
def officeblock(wallmaterial):
"""Create a repeating pattern of 2x2 windows."""
def f(point):
goodx = (point.x%5 == 1) or (point.x%5 == 2)
goody = (point.y%5 == 1) or (point.y%5 == 2)
goodz = (point.z%5 == 1) or (point.z%5 == 2)
if (goodx and goody) or (goodz and goody):
return mc.GLASS
else:
return wallmaterial
return f
def image(path, w, h):
"""Scale the image to the given size."""
img = pygame.image.load(path)
width = img.get_width()
height = img.get_height()
scale_x = width/w
scale_y = height/h
def f(point):
x = int(scale_x/2) + scale_x*point.x
y = height - int(scale_y/2) - scale_y*point.y
material = None
# Anti-aliasing means that some pixels are a mix of colours.
# Keep trying until we get one we can deal with.
while material == None:
r,g,b,a = img.get_at((x,y))
material = tomaterial(r,g,b)
x += 1
return material
return f
def tomaterial(r,g,b):
# Just a quick hack for now - could of course add more colours
# and a way of finding the nearest supported colour.
if (r,g,b) == (255,255,255): # white
return mc.AIR
elif (r,g,b) == (0,0,0): # black
return mc.OBSIDIAN
elif (r,g,b) == (188,17,66): # pink
return mc.REDSTONE_ORE
elif (r,g,b) == (117,169,40): # green
return mc.MELON
else:
return None
## Transformation functions
def identity():
return transformation([[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[0,0,0,1]])
def shift(x,y,z):
"""Move by a given offset."""
return transformation([[1,0,0,x],
[0,1,0,y],
[0,0,1,z],
[0,0,0,1]])
def rotationx(angle):
"""Rotate about the x axis by the given number of degrees."""
angle = radians(angle)
return transformation([[1, 0, 0, 0],
[0, cos(angle), sin(angle), 0],
[0, -sin(angle), cos(angle), 0],
[0, 0, 0, 1]])
def rotationy(angle):
"""Rotate about the y axis by the given number of degrees."""
angle = radians(angle)
return transformation([[ cos(angle), 0, sin(angle), 0],
[ 0, 1, 0, 0],
[-sin(angle), 0, cos(angle), 0],
[ 0, 0, 0, 1]])
def rotationz(angle):
"""Rotate about the z axis by the given number of degrees."""
angle = radians(angle)
return transformation([[ cos(angle), sin(angle), 0, 0],
[-sin(angle), cos(angle), 0, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1]])
## Other functions
def fillshape(shape, transform=identity(), material=None,fillfunc=None):
"""Build a shape in the Minecraft world.
shape must be iterable: it can be a list, tuple, etc., or a generator function.
transform is of type transformation - multiple transformations can be combined
by multiplying them together.
material or fillfunc specify which material(s) to build the shape out of."""
if fillfunc == None:
fillfunc = solid(material)
for point in shape:
point2 = transform * point
mc.setblock(int(point2.x), int(point2.y), int(point2.z), fillfunc(point))
def clear(shape, transform=identity()):
"""Remove any non-air blocks in the given shape."""
fillshape(shape,transform,mc.AIR)
def main():
"""Function used to build my demo world. Extra clearing may be required for
hilly worlds."""
mc.connect(server.address)
# Create a large empty space with a neat, grassy floor. Takes a long time!
clear(cuboid(100,10,120))
fillshape(floor(100,120), shift(0,-1,0), material=mc.GRASS)
# Introduce basic shapes/transformations/fill functions.
fillshape(arrow, material=mc.STONE)
fillshape(arrow, shift(6,0,0), mc.STONE)
fillshape(arrow, shift(12,0,0)*rotationx(90), mc.STONE)
fillshape(arrow, shift(18,0,0)*rotationx(45), mc.STONE)
fillshape(arrow, shift(24,0,0), fillfunc=chequers(mc.WOOD, mc.STONE))
# Introduce generator functions.
fillshape(cuboid(4,4,4), shift(30,0,0), mc.STONE)
fillshape(cuboid(3,8,2), shift(36,0,0), mc.STONE)
# Show other simple shapes.
fillshape(sphere(5), shift(45,5,0), mc.STONE)
fillshape(pyramid(5), shift(50,0,0), mc.STONE)
fillshape(cylinder(5,4), shift(65,0,0), mc.STONE)
fillshape(cone(5,5), shift(75,0,0), mc.STONE)
# Show some fill functions.
fillshape(cuboid(4,4,4), shift(80,0,5), fillfunc=chequers(mc.GOLD, mc.IRON))
fillshape(pyramid(5), shift(80,0,10), fillfunc=randomfill([mc.SAND, mc.SANDSTONE]))
fillshape(hollowcuboid(4,6,4), shift(80,0,22), mc.WOOD_PLANK)
fillshape(building(2,6,2), shift(80,0,30), fillfunc=officeblock(mc.COBBLESTONE))
# Line drawing.
fillshape(line(80,0,40,85,5,45), material=mc.WOOL)
fillshape(line(80,0,40,80,2,50), material=mc.WOOL)
fillshape(line(80,2,50,85,5,45), material=mc.WOOL)
# Fun lava sphere.
fillshape(sphere(10), shift(80,10,60), mc.GLASS)
fillshape(sphere(9), shift(80,10,60), mc.LAVA)
# Fractals - far easier to code than to build by hand.
fillshape(mengersponge(0), shift(70,0,75), mc.IRON)
fillshape(mengersponge(1), shift(66,0,75), mc.IRON)
fillshape(mengersponge(2), shift(56,0,75), mc.IRON)
fillshape(mengersponge(3), shift(28,0,75), mc.IRON)
# Maze.
fillshape(maze(25,25), shift(0,0,75), mc.STONE)
# Picture - can use the same technique to draw text.
# fillshape(cuboid(24,30,1), shift(0,0,30), fillfunc=image("pi.png",24,30))
if __name__ == "__main__":
main()