5. Recipes¶
5.1. Auto Bridge¶
This recipe (and several others in this chapter) was shamelessly stolen from Martin O’Hanlon’s excellent site which includes lots of recipes (although at the time of writing they’re all for the mcpi API). In this case the original script can be found in Martin’s auto-bridge project.
The script tracks the position and likely future position of the player as they walk through the world. If the script detects the player is about to walk onto air it changes the block to diamond:
from __future__ import unicode_literals
import time
from picraft import World, Vector, Block
from collections import deque
world = World(ignore_errors=True)
world.say('Auto-bridge active')
try:
bridge = deque()
last_pos = None
while True:
this_pos = world.player.pos
if last_pos is not None:
# Has the player moved more than 0.1 units in a horizontal direction?
movement = (this_pos - last_pos).replace(y=0.0)
if movement.magnitude > 0.1:
# Find the next tile they're going to step on
next_pos = (this_pos + movement.unit).floor() - Vector(y=1)
if world.blocks[next_pos] == Block('air'):
with world.connection.batch_start():
bridge.append(next_pos)
world.blocks[next_pos] = Block('diamond_block')
while len(bridge) > 10:
world.blocks[bridge.popleft()] = Block('air')
last_pos = this_pos
time.sleep(0.01)
except KeyboardInterrupt:
world.say('Auto-bridge deactivated')
with world.connection.batch_start():
while bridge:
world.blocks[bridge.popleft()] = Block('air')
Note that the script starts by initializing the connection with the
ignore_errors=True
parameter. This causes the picraft library to act like
the mcpi library: errors in “set” calls are ignored, but the library reacts
faster because of this. This is necessary in a script like this where rapid
reaction to player behaviour is required.
5.2. Shapes¶
This recipe demonstrates drawing shapes with blocks in the Minecraft world. The picraft library includes a couple of rudimentary routines for calculating the points necessary for drawing lines:
line()
which can be used to calculate the positions along a single linelines()
which calculates the positions along a series of lines
Here we will attempt to construct a script which draws each regular polygon from an equilateral triangle up to a regular octagon. First we start by defining a function which will generate the points of a regular polygon. This is relatively simple: the interior angles of a polygon always add up to 180 degrees so the angle to turn each time is 180 divided by the number of sides. Given an origin and a side-length it’s a simple matter to iterate over each side generating the necessary point:
from __future__ import division
import math
from picraft import World, Vector, O, X, Y, Z, lines
def polygon(sides, center=O, radius=5):
angle = 2 * math.pi / sides
for side in range(sides):
yield Vector(
center.x + radius * math.cos(side * angle),
center.y + radius * math.sin(side * angle))
print(list(polygon(3, center=3*Y)))
print(list(polygon(4, center=3*Y)))
print(list(polygon(5, center=3*Y)))
Next we need a function which will iterate over the number of sides for each
required polygon, using the lines()
function to generate
the points required to draw the shape. Then it’s a simple matter to draw each
polygon in turn, wiping it before displaying the next one:
from __future__ import division
import math
from picraft import World, Vector, O, X, Y, Z, lines
def polygon(sides, center=O, radius=5):
angle = 2 * math.pi / sides
for side in range(sides):
yield Vector(
center.x + radius * math.cos(side * angle),
center.y + radius * math.sin(side * angle))
def shapes():
for sides in range(3, 9):
yield lines(polygon(sides, center=3*Y))
w = World()
for shape in shapes():
# Draw the shape
with w.connection.batch_start():
for p in shape:
w.blocks[p] = Block('stone')
sleep(0.5)
# Wipe the shape
with w.connection.batch_start():
for p in shape:
w.blocks[p] = Block('air')
5.3. Animation¶
This recipe demonstrates, in a series of steps, the construction of a
simplistic animation system in Minecraft. Our aim is to create a simple stone
cube which rotates about the X axis somewhere in the air. Our first script uses
vector_range()
to obtain the coordinates of the cube,
then uses the rotate()
method to rotate them about
the X axis.
We represent the state of a frame of our animation as a dict which maps
coordinates (in the form of Vector
instances) to
Block
instances:
from __future__ import division
from time import sleep
from picraft import World, Vector, X, Y, Z, vector_range, Block
world = World()
world.checkpoint.save()
try:
cube_range = vector_range(Vector() - 2, Vector() + 2 + 1)
# Draw frame 1
state = {}
for v in cube_range:
state[v + (5 * Y)] = Block('stone')
with world.connection.batch_start():
for v, b in state.items():
world.blocks[v] = b
sleep(0.2)
# Wipe frame 1
with world.connection.batch_start():
for v in state:
world.blocks[v] = Block('air')
# Draw frame 2
state = {}
for v in cube_range:
state[v.rotate(15, about=X).round() + (5 * Y)] = Block('stone')
with world.connection.batch_start():
for v, b in state.items():
world.blocks[v] = b
sleep(0.2)
# and so on...
finally:
world.checkpoint.restore()
As you can see in the script above we draw the first frame, wait for a bit, then wipe the frame by setting all coordinates in that frame’s state back to “air”. Then we draw the second frame and wait for a bit.
Although this approach works, it’s obviously very long winded for lots of
frames. What we want to do is calculate the state of each frame in a function.
This next version demonstrates this approach; we use a generator function to
yield the state of each frame in turn so we can iterate over the frames with
a simple for
loop:
from __future__ import division
from time import sleep
from picraft import World, Vector, X, Y, Z, vector_range, Block
def animation_frames(count):
cube_range = vector_range(Vector() - 2, Vector() + 2 + 1)
for frame in range(count):
state = {}
for v in cube_range:
state[v.rotate(15 * frame, about=X).round() + (5 * Y)] = Block('stone')
yield state
world = World()
world.checkpoint.save()
try:
for frame in animation_frames(10):
# Draw frame
with world.connection.batch_start():
for v, b in frame.items():
world.blocks[v] = b
sleep(0.2)
# Wipe frame
with world.connection.batch_start():
for v, b in frame.items():
world.blocks[v] = Block('air')
finally:
world.checkpoint.restore()
That’s more like it, but the updates aren’t terribly fast despite using the batch functionality. In order to improve this we should only update those blocks which have actually changed between each frame. Thankfully, because we’re storing the state of each as a dict, this is quite easy:
from __future__ import division
from time import sleep
from picraft import World, Vector, X, Y, Z, vector_range, Block
def animation_frames(count):
cube_range = vector_range(Vector() - 2, Vector() + 2 + 1)
for frame in range(count):
yield {
v.rotate(15 * frame, about=X).round() + (5 * Y): Block('stone')
for v in cube_range}
def track_changes(states, default=Block('air')):
old_state = None
for state in states:
# Assume the initial state of the blocks is the default ('air')
if old_state is None:
old_state = {v: default for v in state}
# Build a dict of those blocks which changed from old_state to state
changes = {v: b for v, b in state.items() if old_state.get(v) != b}
# Blank out blocks which were in old_state but aren't in state
changes.update({v: default for v in old_state if v not in state})
yield changes
old_state = state
world = World()
world.checkpoint.save()
try:
for state in track_changes(animation_frames(20)):
with world.connection.batch_start():
for v, b in state.items():
world.blocks[v] = b
sleep(0.2)
finally:
world.checkpoint.restore()
Note: this still isn’t perfect. Ideally, we would identify contiguous blocks of coordinates to be updated which have the same block and set them all at the same time (which will utilize the world.setBlocks call for efficiency). However, this is relatively complex to do well so I shall leave it as an exercise for you, dear reader!
5.4. Minecraft TV¶
If you’ve got a Raspberry Pi camera module, you can build a TV to view a live feed from the camera in the Minecraft world. Firstly we need to construct a class which will accept JPEGs from the camera’s MJPEG stream, and render them as blocks in the Minecraft world. Then we need a class to construct the TV model itself and enable interaction with it:
from __future__ import division
import io
import time
import picamera
from picraft import World, V, Block
from picraft.block import _BLOCKS_BY_COLOR
from PIL import Image
class MinecraftTVScreen(object):
def __init__(self, world, origin, size):
self.world = world
self.origin = origin
self.size = size
self.jpeg = None
# Construct a palette for PIL
self.palette = Image.new('P', (1, 1))
self.palette_len = len(_BLOCKS_BY_COLOR)
PALETTE = {data: color for color, (id, data) in _BLOCKS_BY_COLOR.items()}
PALETTE = [PALETTE[i] for i in range(16)]
self.palette.putpalette(
[c for rgb in PALETTE for c in rgb] +
list(PALETTE[0]) * (256 - len(PALETTE))
)
def write(self, buf):
if buf.startswith(b'\xff\xd8'):
if self.jpeg:
self.jpeg.seek(0)
self.render(self.jpeg)
self.jpeg = io.BytesIO()
self.jpeg.write(buf)
def close(self):
self.jpeg = None
def render(self, jpeg):
o = self.origin
img = Image.open(jpeg)
img = img.resize(self.size, Image.BILINEAR)
img = img.quantize(self.palette_len, palette=self.palette)
with self.world.connection.batch_start():
for x in range(img.size[0]):
for y in range(img.size[1]):
self.world.blocks[o + V(0, y, x)] = Block.from_id(35, img.getpixel((x, y)))
class MinecraftTV(object):
def __init__(self, origin=V(), size=(12, 8)):
self.camera = picamera.PiCamera()
self.camera.resolution = (64, int(64 / size[0] * size[1]))
self.camera.framerate = 2
self.world = World(ignore_errors=True)
self.origin = origin
self.size = V(0, size[1], size[0])
self.button_vec = None
self.screen = MinecraftTVScreen(
self.world, origin + V(0, 1, 1), (size[0] - 2, size[1] - 2))
def main_loop(self):
self.create_tv()
try:
while True:
for event in self.world.events.poll():
if event.pos == self.button_vec:
if self.camera.recording:
self.switch_off()
else:
self.switch_on()
time.sleep(0.1)
finally:
if self.camera.recording:
self.switch_off()
self.destroy_tv()
def create_tv(self):
o = self.origin
self.world.blocks[o:o + self.size + 1] = Block('#ffffff')
self.world.blocks[
o + V(0, 1, 1):o + self.size - V(0, 1, 1) + 1] = Block('#000000')
self.button_vec = o + V(z=2)
self.world.blocks[self.button_vec] = Block('#800000')
def destroy_tv(self):
o = self.origin
self.world.blocks[o:o + self.size + 1] = Block('air')
def switch_on(self):
self.camera.start_recording(self.screen, format='mjpeg')
def switch_off(self):
self.camera.stop_recording()
o = self.origin
self.world.blocks[
o + V(0, 1, 1):o + self.size - V(0, 2, 2) + 1] = Block('#000000')
tv = MinecraftTV(origin=V(2, 0, 5), size=(24,16))
tv.main_loop()
Don’t expect to be able to recognize much in the Minecraft TV; the resolution is extremely low and the color matching is far from perfect. Still, if you point the camera at obvious blocks of primary colors and move it around slowly you should see a similar result on the in-game display.
The script includes the ability to position and size the TV as you like, and you may like to experiment with adding new controls to it!