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:

#!/usr/bin/env python

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. 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:

#!/usr/bin/env python

from __future__ import division

import io
import time
import picamera
from picraft import World, Vector as 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')


def main():
    tv = MinecraftTV(origin=V(2, 0, 5), size=(24,16))
    tv.main_loop()


if __name__ == '__main__':
    main()

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!