439 lines
13 KiB
Python
Executable File
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() |